mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:51:43 +00:00
use DOMPurify instead of sanitize-html for smaller bundle (#50002)
Also sanitize HTML more strictly. Previously we allowed SVG and `data:` URIs in some cases (for some functionality from the legacy Sourcegraph extension API). This is no longer needed, and getting stricter in HTML sanitization is generally good. ## Test plan Test callers of renderMarkdown in UI. Co-authored-by: Juliana Peña <me@julip.co>
This commit is contained in:
parent
691b119404
commit
972636de77
2
client/branded/BUILD.bazel
generated
2
client/branded/BUILD.bazel
generated
@ -169,7 +169,6 @@ ts_project(
|
||||
"//:node_modules/@types/node",
|
||||
"//:node_modules/@types/react",
|
||||
"//:node_modules/@types/react-dom",
|
||||
"//:node_modules/@types/sanitize-html",
|
||||
"//:node_modules/classnames",
|
||||
"//:node_modules/comlink", #keep
|
||||
"//:node_modules/copy-to-clipboard",
|
||||
@ -184,7 +183,6 @@ ts_project(
|
||||
"//:node_modules/react-sticky-box",
|
||||
"//:node_modules/react-visibility-sensor",
|
||||
"//:node_modules/rxjs",
|
||||
"//:node_modules/sanitize-html",
|
||||
"//:node_modules/ts-key-enum",
|
||||
"//:node_modules/use-resize-observer",
|
||||
"//client/shared:shared_lib", #keep
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { range } from 'lodash'
|
||||
import { of } from 'rxjs'
|
||||
import { catchError } from 'rxjs/operators'
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
|
||||
import { highlightNode, logger } from '@sourcegraph/common'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
@ -47,7 +47,7 @@ export const CommitSearchResultMatch: React.FunctionComponent<CommitSearchResult
|
||||
// Return the rendered markdown if highlighting fails.
|
||||
catchError(error => {
|
||||
logger.log(error)
|
||||
return of('<pre>' + sanitizeHtml(item.content) + '</pre>')
|
||||
return of('<pre>' + DOMPurify.sanitize(item.content) + '</pre>')
|
||||
})
|
||||
)
|
||||
.subscribe(highlightedCommitContent => {
|
||||
|
||||
@ -67,8 +67,6 @@ exports[`StreamingProgressSkippedPopover should render correctly 1`] = `
|
||||
<p>
|
||||
There was a network error retrieving search results. Check your Internet connection and try again.
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -130,8 +128,6 @@ exports[`StreamingProgressSkippedPopover should render correctly 1`] = `
|
||||
<p>
|
||||
Search timed out
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -193,8 +189,6 @@ exports[`StreamingProgressSkippedPopover should render correctly 1`] = `
|
||||
<p>
|
||||
10k forked repositories excluded
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -256,8 +250,6 @@ exports[`StreamingProgressSkippedPopover should render correctly 1`] = `
|
||||
<p>
|
||||
60k archived repositories excluded
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -323,8 +315,6 @@ exports[`StreamingProgressSkippedPopover should render correctly 1`] = `
|
||||
</code>
|
||||
in your query.
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@ -16,5 +16,8 @@
|
||||
"lint": "pnpm run lint:js",
|
||||
"lint:js": "eslint --cache '**/*.[tj]s?(x)'",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sourcegraph/common": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,27 +1,16 @@
|
||||
import { marked } from 'marked'
|
||||
import sanitize from 'sanitize-html'
|
||||
import { registerHighlightContributions, renderMarkdown as renderMarkdownCommon } from '@sourcegraph/common'
|
||||
|
||||
import { highlightCode, registerHighlightContributions } from './highlight'
|
||||
|
||||
const sanitizeOptions = {
|
||||
...sanitize.defaults,
|
||||
allowedAttributes: {
|
||||
...sanitize.defaults.allowedAttributes,
|
||||
a: [
|
||||
...sanitize.defaults.allowedAttributes.a,
|
||||
'title',
|
||||
'class',
|
||||
{ name: 'rel', values: ['noopener', 'noreferrer'] },
|
||||
],
|
||||
// Allow highlight.js styles, e.g.
|
||||
// <span class="hljs-keyword">
|
||||
// <code class="language-javascript">
|
||||
span: ['class'],
|
||||
code: ['class'],
|
||||
},
|
||||
}
|
||||
|
||||
export function renderMarkdown(text: string): string {
|
||||
/**
|
||||
* Render Markdown to safe HTML.
|
||||
*
|
||||
* NOTE: This only works when called in an environment with the DOM. In the VS Code extension, it
|
||||
* only works in the webview context, not in the extension host context, because the latter lacks a
|
||||
* DOM. We could use isomorphic-dompurify for that, but that adds needless complexity for now. If
|
||||
* that becomes necessary, we can add that.
|
||||
*/
|
||||
export function renderMarkdown(markdown: string): string {
|
||||
registerHighlightContributions()
|
||||
return sanitize(marked.parse(text, { gfm: true, highlight: highlightCode, breaks: true }), sanitizeOptions)
|
||||
|
||||
// Add Cody-specific Markdown rendering if needed.
|
||||
return renderMarkdownCommon(markdown)
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import { MAX_CURRENT_FILE_TOKENS, MAX_HUMAN_INPUT_TOKENS } from '../../prompt/co
|
||||
import { populateCodeContextTemplate } from '../../prompt/templates'
|
||||
import { truncateText } from '../../prompt/truncation'
|
||||
import { getShortTimestamp } from '../../timestamp'
|
||||
import { renderMarkdown } from '../markdown'
|
||||
import { Interaction } from '../transcript/interaction'
|
||||
|
||||
import { Recipe } from './recipe'
|
||||
@ -24,11 +23,10 @@ export class ChatQuestion implements Recipe {
|
||||
): Promise<Interaction | null> {
|
||||
const timestamp = getShortTimestamp()
|
||||
const truncatedText = truncateText(humanChatInput, MAX_HUMAN_INPUT_TOKENS)
|
||||
const displayText = renderMarkdown(humanChatInput)
|
||||
|
||||
return Promise.resolve(
|
||||
new Interaction(
|
||||
{ speaker: 'human', text: truncatedText, displayText, timestamp },
|
||||
{ speaker: 'human', text: truncatedText, displayText: humanChatInput, timestamp },
|
||||
{ speaker: 'assistant', text: '', displayText: '', timestamp },
|
||||
this.getContextMessages(truncatedText, editor, intentDetector, codebaseContext)
|
||||
)
|
||||
|
||||
@ -4,7 +4,6 @@ import { IntentDetector } from '../../intent-detector'
|
||||
import { MAX_RECIPE_INPUT_TOKENS, MAX_RECIPE_SURROUNDING_TOKENS } from '../../prompt/constants'
|
||||
import { truncateText, truncateTextStart } from '../../prompt/truncation'
|
||||
import { getShortTimestamp } from '../../timestamp'
|
||||
import { renderMarkdown } from '../markdown'
|
||||
import { Interaction } from '../transcript/interaction'
|
||||
|
||||
import { getContextMessagesFromSelection, getNormalizedLanguageName, MARKDOWN_FORMAT_PROMPT } from './helpers'
|
||||
@ -33,7 +32,7 @@ export class ExplainCodeDetailed implements Recipe {
|
||||
|
||||
const languageName = getNormalizedLanguageName(selection.fileName)
|
||||
const promptMessage = `Please explain the following ${languageName} code. Be very detailed and specific, and indicate when it is not clear to you what is going on. Format your response as an ordered list.\n\`\`\`\n${truncatedSelectedText}\n\`\`\`\n${MARKDOWN_FORMAT_PROMPT}`
|
||||
const displayText = renderMarkdown(`Explain the following code:\n\`\`\`\n${selection.selectedText}\n\`\`\``)
|
||||
const displayText = `Explain the following code:\n\`\`\`\n${selection.selectedText}\n\`\`\``
|
||||
|
||||
return new Interaction(
|
||||
{ speaker: 'human', text: promptMessage, displayText, timestamp },
|
||||
|
||||
@ -4,7 +4,6 @@ import { IntentDetector } from '../../intent-detector'
|
||||
import { MAX_RECIPE_INPUT_TOKENS, MAX_RECIPE_SURROUNDING_TOKENS } from '../../prompt/constants'
|
||||
import { truncateText, truncateTextStart } from '../../prompt/truncation'
|
||||
import { getShortTimestamp } from '../../timestamp'
|
||||
import { renderMarkdown } from '../markdown'
|
||||
import { Interaction } from '../transcript/interaction'
|
||||
|
||||
import { getContextMessagesFromSelection, getNormalizedLanguageName, MARKDOWN_FORMAT_PROMPT } from './helpers'
|
||||
@ -33,9 +32,7 @@ export class ExplainCodeHighLevel implements Recipe {
|
||||
|
||||
const languageName = getNormalizedLanguageName(selection.fileName)
|
||||
const promptMessage = `Explain the following ${languageName} code at a high level. Only include details that are essential to an overal understanding of what's happening in the code.\n\`\`\`\n${truncatedSelectedText}\n\`\`\`\n${MARKDOWN_FORMAT_PROMPT}`
|
||||
const displayText = renderMarkdown(
|
||||
`Explain the following code at a high level:\n\`\`\`\n${selection.selectedText}\n\`\`\``
|
||||
)
|
||||
const displayText = `Explain the following code at a high level:\n\`\`\`\n${selection.selectedText}\n\`\`\``
|
||||
|
||||
return new Interaction(
|
||||
{ speaker: 'human', text: promptMessage, displayText, timestamp },
|
||||
|
||||
@ -4,7 +4,6 @@ import { IntentDetector } from '../../intent-detector'
|
||||
import { MAX_RECIPE_INPUT_TOKENS, MAX_RECIPE_SURROUNDING_TOKENS } from '../../prompt/constants'
|
||||
import { truncateText, truncateTextStart } from '../../prompt/truncation'
|
||||
import { getShortTimestamp } from '../../timestamp'
|
||||
import { renderMarkdown } from '../markdown'
|
||||
import { Interaction } from '../transcript/interaction'
|
||||
|
||||
import {
|
||||
@ -56,9 +55,7 @@ export class GenerateDocstring implements Recipe {
|
||||
docStart = '// '
|
||||
}
|
||||
|
||||
const displayText = renderMarkdown(
|
||||
`Generate documentation for the following code:\n\`\`\`\n${selection.selectedText}\n\`\`\``
|
||||
)
|
||||
const displayText = `Generate documentation for the following code:\n\`\`\`\n${selection.selectedText}\n\`\`\``
|
||||
|
||||
const assistantResponsePrefix = `Here is the generated documentation:\n\`\`\`${extension}\n${docStart}`
|
||||
return new Interaction(
|
||||
|
||||
@ -4,7 +4,6 @@ import { IntentDetector } from '../../intent-detector'
|
||||
import { MAX_RECIPE_INPUT_TOKENS, MAX_RECIPE_SURROUNDING_TOKENS } from '../../prompt/constants'
|
||||
import { truncateText, truncateTextStart } from '../../prompt/truncation'
|
||||
import { getShortTimestamp } from '../../timestamp'
|
||||
import { renderMarkdown } from '../markdown'
|
||||
import { Interaction } from '../transcript/interaction'
|
||||
|
||||
import {
|
||||
@ -41,9 +40,7 @@ export class GenerateTest implements Recipe {
|
||||
const promptMessage = `Generate a unit test in ${languageName} for the following code:\n\`\`\`${extension}\n${truncatedSelectedText}\n\`\`\`\n${MARKDOWN_FORMAT_PROMPT}`
|
||||
const assistantResponsePrefix = `Here is the generated unit test:\n\`\`\`${extension}\n`
|
||||
|
||||
const displayText = renderMarkdown(
|
||||
`Generate a unit test for the following code:\n\`\`\`${extension}\n${selection.selectedText}\n\`\`\``
|
||||
)
|
||||
const displayText = `Generate a unit test for the following code:\n\`\`\`${extension}\n${selection.selectedText}\n\`\`\``
|
||||
|
||||
return new Interaction(
|
||||
{ speaker: 'human', text: promptMessage, displayText, timestamp },
|
||||
|
||||
@ -7,7 +7,6 @@ import { IntentDetector } from '../../intent-detector'
|
||||
import { MAX_RECIPE_INPUT_TOKENS } from '../../prompt/constants'
|
||||
import { truncateText } from '../../prompt/truncation'
|
||||
import { getShortTimestamp } from '../../timestamp'
|
||||
import { renderMarkdown } from '../markdown'
|
||||
import { Interaction } from '../transcript/interaction'
|
||||
|
||||
import { Recipe } from './recipe'
|
||||
@ -76,7 +75,7 @@ export class GitHistory implements Recipe {
|
||||
const promptMessage = `Summarize these commits:\n${truncatedGitLogOutput}\n\nProvide your response in the form of a bulleted list. Do not mention the commit hashes.`
|
||||
const assistantResponsePrefix = 'Here is a summary of recent changes:\n- '
|
||||
return new Interaction(
|
||||
{ speaker: 'human', text: promptMessage, displayText: renderMarkdown(rawDisplayText), timestamp },
|
||||
{ speaker: 'human', text: promptMessage, displayText: rawDisplayText, timestamp },
|
||||
{
|
||||
speaker: 'assistant',
|
||||
prefix: assistantResponsePrefix,
|
||||
|
||||
@ -4,7 +4,6 @@ import { IntentDetector } from '../../intent-detector'
|
||||
import { MAX_RECIPE_INPUT_TOKENS, MAX_RECIPE_SURROUNDING_TOKENS } from '../../prompt/constants'
|
||||
import { truncateText, truncateTextStart } from '../../prompt/truncation'
|
||||
import { getShortTimestamp } from '../../timestamp'
|
||||
import { renderMarkdown } from '../markdown'
|
||||
import { Interaction } from '../transcript/interaction'
|
||||
|
||||
import {
|
||||
@ -37,9 +36,7 @@ export class ImproveVariableNames implements Recipe {
|
||||
const truncatedFollowingText = truncateText(selection.followingText, MAX_RECIPE_SURROUNDING_TOKENS)
|
||||
const extension = getFileExtension(selection.fileName)
|
||||
|
||||
const displayText = renderMarkdown(
|
||||
`Improve the variable names in the following code:\n\`\`\`\n${selection.selectedText}\n\`\`\``
|
||||
)
|
||||
const displayText = `Improve the variable names in the following code:\n\`\`\`\n${selection.selectedText}\n\`\`\``
|
||||
|
||||
const languageName = getNormalizedLanguageName(selection.fileName)
|
||||
const promptMessage = `Improve the variable names in this ${languageName} code by replacing the variable names with new identifiers which succinctly capture the purpose of the variable. We want the new code to be a drop-in replacement, so do not change names bound outside the scope of this code, like function names or members defined elsewhere. Only change the names of local variables and parameters:\n\n\`\`\`${extension}\n${truncatedSelectedText}\n\`\`\`\n${MARKDOWN_FORMAT_PROMPT}`
|
||||
|
||||
@ -4,7 +4,6 @@ import { IntentDetector } from '../../intent-detector'
|
||||
import { MAX_RECIPE_INPUT_TOKENS } from '../../prompt/constants'
|
||||
import { truncateText } from '../../prompt/truncation'
|
||||
import { getShortTimestamp } from '../../timestamp'
|
||||
import { renderMarkdown } from '../markdown'
|
||||
import { Interaction } from '../transcript/interaction'
|
||||
|
||||
import { languageMarkdownID, languageNames } from './langs'
|
||||
@ -37,9 +36,7 @@ export class TranslateToLanguage implements Recipe {
|
||||
const truncatedSelectedText = truncateText(selection.selectedText, MAX_RECIPE_INPUT_TOKENS)
|
||||
|
||||
const promptMessage = `Translate the following code into ${toLanguage}\n\`\`\`\n${truncatedSelectedText}\n\`\`\``
|
||||
const displayText = renderMarkdown(
|
||||
`Translate the following code into ${toLanguage}\n\`\`\`\n${selection.selectedText}\n\`\`\``
|
||||
)
|
||||
const displayText = `Translate the following code into ${toLanguage}\n\`\`\`\n${selection.selectedText}\n\`\`\``
|
||||
|
||||
const markdownID = languageMarkdownID[toLanguage] || ''
|
||||
const assistantResponsePrefix = `Here is the code translated to ${toLanguage}:\n\`\`\`${markdownID}\n`
|
||||
|
||||
@ -19,11 +19,11 @@ export class Transcript {
|
||||
return this.interactions.length > 0 ? this.interactions[this.interactions.length - 1] : null
|
||||
}
|
||||
|
||||
public addAssistantResponse(text: string, displayText: string): void {
|
||||
public addAssistantResponse(text: string): void {
|
||||
this.getLastInteraction()?.setAssistantMessage({
|
||||
speaker: 'assistant',
|
||||
text,
|
||||
displayText,
|
||||
displayText: text,
|
||||
timestamp: getShortTimestamp(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ describe('Transcript', () => {
|
||||
transcript.addInteraction(firstInteraction)
|
||||
|
||||
const assistantResponse = 'By setting the Authorization header.'
|
||||
transcript.addAssistantResponse(assistantResponse, assistantResponse)
|
||||
transcript.addAssistantResponse(assistantResponse)
|
||||
|
||||
const secondInteraction = await chatQuestionRecipe.getInteraction(
|
||||
'how to create a batch change',
|
||||
@ -135,7 +135,7 @@ describe('Transcript', () => {
|
||||
transcript.addInteraction(interaction)
|
||||
|
||||
const assistantResponse = 'EFGH'.repeat(256) // 256 tokens
|
||||
transcript.addAssistantResponse(assistantResponse, assistantResponse)
|
||||
transcript.addAssistantResponse(assistantResponse)
|
||||
}
|
||||
|
||||
const tokensPerInteraction = 512 // 256 for question + 256 for response.
|
||||
|
||||
@ -8,4 +8,5 @@
|
||||
},
|
||||
"include": ["**/*", ".*"],
|
||||
"exclude": ["out"],
|
||||
"references": [{ "path": "../common" }],
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
import { ChatClient } from '@sourcegraph/cody-shared/src/chat/chat'
|
||||
import { renderMarkdown } from '@sourcegraph/cody-shared/src/chat/markdown'
|
||||
import { getRecipe } from '@sourcegraph/cody-shared/src/chat/recipes'
|
||||
import { Transcript } from '@sourcegraph/cody-shared/src/chat/transcript'
|
||||
import { ChatMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages'
|
||||
@ -214,7 +213,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
|
||||
private async onBotMessageChange(text: string): Promise<void> {
|
||||
this.transcript.addAssistantResponse(text, renderMarkdown(text))
|
||||
this.transcript.addAssistantResponse(text)
|
||||
await this.sendTranscript()
|
||||
}
|
||||
|
||||
|
||||
@ -79,8 +79,8 @@ suite('End-to-end', () => {
|
||||
|
||||
// Check the chat transcript contains markdown
|
||||
const message = await getTranscript(api, 0)
|
||||
assert.ok(message.displayText.startsWith('<p>Explain the following code'))
|
||||
assert.ok(message.displayText.includes('<span class="hljs-keyword">public</span>'))
|
||||
assert.match(message.displayText, /^Explain the following code/)
|
||||
assert.match(message.displayText, /public/)
|
||||
|
||||
// Check the server response was handled
|
||||
// "hello world" is a canned response from the server
|
||||
@ -90,7 +90,7 @@ suite('End-to-end', () => {
|
||||
return assistantMessage.displayText.length > 0
|
||||
})
|
||||
const assistantMessage = await getTranscript(api, 1)
|
||||
assert.ok(assistantMessage.displayText.includes('hello, world'))
|
||||
assert.match(assistantMessage.displayText, /hello, world/)
|
||||
|
||||
// Clean up.
|
||||
await ensureExecuteCommand('cody.delete-access-token')
|
||||
|
||||
@ -10,7 +10,7 @@ export function run(): Promise<void> {
|
||||
color: true,
|
||||
})
|
||||
// To debug tests interactively, extend this timeout.
|
||||
mocha.timeout(2000)
|
||||
mocha.timeout(15000)
|
||||
|
||||
const testsRoot = __dirname
|
||||
|
||||
|
||||
@ -6,6 +6,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { VSCodeButton, VSCodeTextArea } from '@vscode/webview-ui-toolkit/react'
|
||||
|
||||
import { renderMarkdown } from '@sourcegraph/cody-shared/src/chat/markdown'
|
||||
|
||||
import { Tips } from './Tips'
|
||||
import { SubmitSvg } from './utils/icons'
|
||||
import { ChatMessage } from './utils/types'
|
||||
@ -118,7 +120,11 @@ export const Chat: React.FunctionComponent<React.PropsWithChildren<ChatboxProps>
|
||||
className={`bubble-content ${bubbleClassName(message.speaker)}-bubble-content`}
|
||||
>
|
||||
{message.displayText && (
|
||||
<p dangerouslySetInnerHTML={{ __html: message.displayText }} />
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderMarkdown(message.displayText),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{message.contextFiles && message.contextFiles.length > 0 && (
|
||||
<ContextFiles contextFiles={message.contextFiles} />
|
||||
@ -138,7 +144,11 @@ export const Chat: React.FunctionComponent<React.PropsWithChildren<ChatboxProps>
|
||||
<div className="bubble bot-bubble">
|
||||
<div className="bubble-content bot-bubble-content">
|
||||
{messageInProgress.displayText ? (
|
||||
<p dangerouslySetInnerHTML={{ __html: messageInProgress.displayText }} />
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderMarkdown(messageInProgress.displayText),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="bubble-loader">
|
||||
<div className="bubble-loader-dot" />
|
||||
|
||||
@ -69,40 +69,35 @@ describe('renderMarkdown', () => {
|
||||
<td>B</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<p><img src=\\"./src.jpg\\" alt=\\"image alt text\\" /></p>
|
||||
"
|
||||
<p><img alt=\\"image alt text\\" src=\\"./src.jpg\\"></p>"
|
||||
`)
|
||||
})
|
||||
it('renders to plain text with plainText: true', () => {
|
||||
expect(renderMarkdown('A **b**', { plainText: true })).toBe('A b\n')
|
||||
expect(renderMarkdown('A **b**', { plainText: true })).toBe('A b')
|
||||
})
|
||||
it('sanitizes script tags', () => {
|
||||
expect(renderMarkdown('<script>evil();</script>')).toBe('')
|
||||
})
|
||||
it('sanitizes event handlers', () => {
|
||||
expect(renderMarkdown('<svg><rect onclick="evil()"></rect></svg>')).toBe('<p><svg><rect></rect></svg></p>\n')
|
||||
expect(renderMarkdown('<b onclick="evil()">test</b></svg>')).toBe('<p><b>test</b></p>')
|
||||
})
|
||||
it('does not allow arbitrary <object> tags', () => {
|
||||
expect(renderMarkdown('<object data="something"></object>')).toBe('<p></p>\n')
|
||||
expect(renderMarkdown('<object data="something"></object>')).toBe('<p></p>')
|
||||
})
|
||||
it('drops SVG <object> tags', () => {
|
||||
expect(renderMarkdown('<object data="something" type="image/svg+xml"></object>')).toBe('<p></p>\n')
|
||||
expect(renderMarkdown('<object data="something" type="image/svg+xml"></object>')).toBe('<p></p>')
|
||||
})
|
||||
it('allows <svg> tags', () => {
|
||||
it('forbids <svg> tags', () => {
|
||||
const input =
|
||||
'<svg viewbox="10 10 10 10" width="100"><rect x="37.5" y="7.5" width="675.0" height="16.875" fill="#e05d44" stroke="white" stroke-width="1"><title>/</title></rect></svg>'
|
||||
expect(renderMarkdown(input)).toBe(`<p>${input}</p>\n`)
|
||||
expect(renderMarkdown(input)).toBe('<p></p>')
|
||||
})
|
||||
|
||||
describe('allowDataUriLinksAndDownloads option', () => {
|
||||
const MARKDOWN_WITH_DOWNLOAD = '<a href="data:text/plain,foobar" download>D</a>\n[D2](data:text/plain,foobar)'
|
||||
test('default disabled', () => {
|
||||
expect(renderMarkdown(MARKDOWN_WITH_DOWNLOAD)).toBe('<p><a>D</a>\n<a>D2</a></p>\n')
|
||||
})
|
||||
test('enabled', () => {
|
||||
expect(renderMarkdown(MARKDOWN_WITH_DOWNLOAD, { allowDataUriLinksAndDownloads: true })).toBe(
|
||||
'<p><a href="data:text/plain,foobar" download>D</a>\n<a href="data:text/plain,foobar">D2</a></p>\n'
|
||||
)
|
||||
})
|
||||
it('forbids rel and style attributes', () => {
|
||||
const input = '<a href="/" rel="evil" style="foo:bar">Link</a><script>alert("x")</script>'
|
||||
expect(renderMarkdown(input)).toBe('<p><a href="/">Link</a></p>')
|
||||
})
|
||||
test('forbids data URI links', () => {
|
||||
const input = '<a href="data:text/plain,foobar" download>D</a>\n[D2](data:text/plain,foobar)'
|
||||
expect(renderMarkdown(input)).toBe('<p><a download="">D</a>\n<a>D2</a></p>')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import DOMPurify, { Config as DOMPurifyConfig } from 'dompurify'
|
||||
import { highlight, highlightAuto } from 'highlight.js/lib/core'
|
||||
import { without } from 'lodash'
|
||||
// This is the only file allowed to import this module, all other modules must use renderMarkdown() exported from here
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { marked } from 'marked'
|
||||
import sanitize from 'sanitize-html'
|
||||
import { Overwrite } from 'utility-types'
|
||||
|
||||
import { logger } from '../logger'
|
||||
|
||||
@ -44,15 +42,11 @@ export const highlightCodeSafe = (code: string, language?: string): string => {
|
||||
}
|
||||
}
|
||||
|
||||
const svgPresentationAttributes = ['fill', 'stroke', 'stroke-width'] as const
|
||||
const ALL_VALUES_ALLOWED = [/.*/]
|
||||
|
||||
/**
|
||||
* Renders the given markdown to HTML, highlighting code and sanitizing dangerous HTML.
|
||||
* Can throw an exception on parse errors.
|
||||
*
|
||||
* @param markdown The markdown to render
|
||||
* @param options Options object for passing additional parameters
|
||||
*/
|
||||
export const renderMarkdown = (
|
||||
markdown: string,
|
||||
@ -63,19 +57,9 @@ export const renderMarkdown = (
|
||||
disableAutolinks?: boolean
|
||||
renderer?: marked.Renderer
|
||||
headerPrefix?: string
|
||||
} & (
|
||||
| {
|
||||
/** Strip off any HTML and return a plain text string, useful for previews */
|
||||
plainText: true
|
||||
}
|
||||
| {
|
||||
/** Following options apply to non-plaintext output only. */
|
||||
plainText?: false
|
||||
|
||||
/** Allow links to data: URIs and that download files. */
|
||||
allowDataUriLinksAndDownloads?: boolean
|
||||
}
|
||||
) = {}
|
||||
/** Strip off any HTML and return a plain text string, useful for previews */
|
||||
plainText?: boolean
|
||||
} = {}
|
||||
): string => {
|
||||
const tokenizer = new marked.Tokenizer()
|
||||
if (options.disableAutolinks) {
|
||||
@ -96,94 +80,18 @@ export const renderMarkdown = (
|
||||
tokenizer,
|
||||
})
|
||||
|
||||
let sanitizeOptions: Overwrite<sanitize.IOptions, sanitize.IDefaults>
|
||||
if (options.plainText) {
|
||||
sanitizeOptions = {
|
||||
...sanitize.defaults,
|
||||
allowedAttributes: {},
|
||||
allowedSchemes: [],
|
||||
allowedSchemesByTag: {},
|
||||
allowedTags: [],
|
||||
selfClosing: [],
|
||||
}
|
||||
} else {
|
||||
sanitizeOptions = {
|
||||
...sanitize.defaults,
|
||||
// Ensure <object> must have type attribute set
|
||||
exclusiveFilter: ({ tag, attribs }) => tag === 'object' && !attribs.type,
|
||||
const dompurifyConfig: DOMPurifyConfig & { RETURN_DOM_FRAGMENT?: false; RETURN_DOM?: false } = options.plainText
|
||||
? {
|
||||
ALLOWED_TAGS: [],
|
||||
ALLOWED_ATTR: [],
|
||||
KEEP_CONTENT: true,
|
||||
}
|
||||
: {
|
||||
USE_PROFILES: { html: true },
|
||||
FORBID_ATTR: ['rel', 'style'],
|
||||
}
|
||||
|
||||
allowedTags: [
|
||||
...without(sanitize.defaults.allowedTags, 'iframe'),
|
||||
'img',
|
||||
'picture',
|
||||
'source',
|
||||
'svg',
|
||||
'rect',
|
||||
'defs',
|
||||
'pattern',
|
||||
'mask',
|
||||
'circle',
|
||||
'path',
|
||||
'title',
|
||||
],
|
||||
allowedAttributes: {
|
||||
...sanitize.defaults.allowedAttributes,
|
||||
a: [
|
||||
...sanitize.defaults.allowedAttributes.a,
|
||||
'title',
|
||||
'class',
|
||||
{ name: 'rel', values: ['noopener', 'noreferrer'] },
|
||||
],
|
||||
img: [...sanitize.defaults.allowedAttributes.img, 'alt', 'width', 'height', 'align', 'style'],
|
||||
// Support different images depending on media queries (e.g. color theme, reduced motion)
|
||||
source: ['srcset', 'media'],
|
||||
svg: ['width', 'height', 'viewbox', 'version', 'preserveaspectratio', 'style'],
|
||||
rect: ['x', 'y', 'width', 'height', 'transform', ...svgPresentationAttributes],
|
||||
path: ['d', ...svgPresentationAttributes],
|
||||
circle: ['cx', 'cy', ...svgPresentationAttributes],
|
||||
pattern: ['id', 'width', 'height', 'patternunits', 'patterntransform'],
|
||||
mask: ['id'],
|
||||
// Allow highligh.js styles, e.g.
|
||||
// <span class="hljs-keyword">
|
||||
// <code class="language-javascript">
|
||||
span: ['class'],
|
||||
code: ['class'],
|
||||
// Support deep-linking
|
||||
h1: ['id'],
|
||||
h2: ['id'],
|
||||
h3: ['id'],
|
||||
h4: ['id'],
|
||||
h5: ['id'],
|
||||
h6: ['id'],
|
||||
},
|
||||
allowedStyles: {
|
||||
img: {
|
||||
padding: ALL_VALUES_ALLOWED,
|
||||
'padding-left': ALL_VALUES_ALLOWED,
|
||||
'padding-right': ALL_VALUES_ALLOWED,
|
||||
'padding-top': ALL_VALUES_ALLOWED,
|
||||
'padding-bottom': ALL_VALUES_ALLOWED,
|
||||
},
|
||||
// SVGs are usually for charts in code insights.
|
||||
// Allow them to be responsive.
|
||||
svg: {
|
||||
flex: ALL_VALUES_ALLOWED,
|
||||
'flex-grow': ALL_VALUES_ALLOWED,
|
||||
'flex-shrink': ALL_VALUES_ALLOWED,
|
||||
'flex-basis': ALL_VALUES_ALLOWED,
|
||||
},
|
||||
},
|
||||
}
|
||||
if (options.allowDataUriLinksAndDownloads) {
|
||||
sanitizeOptions.allowedAttributes.a = [...sanitizeOptions.allowedAttributes.a, 'download']
|
||||
sanitizeOptions.allowedSchemesByTag = {
|
||||
...sanitizeOptions.allowedSchemesByTag,
|
||||
a: [...(sanitizeOptions.allowedSchemesByTag.a || sanitizeOptions.allowedSchemes), 'data'],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sanitize(rendered, sanitizeOptions)
|
||||
return DOMPurify.sanitize(rendered, dompurifyConfig).trim()
|
||||
}
|
||||
|
||||
export const markdownLexer = (markdown: string): marked.TokensList => marked.lexer(markdown)
|
||||
|
||||
@ -98,8 +98,6 @@ exports[`HoverOverlay actions and hover present 1`] = `
|
||||
<p>
|
||||
v
|
||||
</p>
|
||||
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@ -164,8 +162,6 @@ exports[`HoverOverlay actions error, hover present 1`] = `
|
||||
<p>
|
||||
v
|
||||
</p>
|
||||
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@ -388,8 +384,6 @@ exports[`HoverOverlay hover present 1`] = `
|
||||
<p>
|
||||
v
|
||||
</p>
|
||||
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@ -417,8 +411,6 @@ exports[`HoverOverlay hover present, actions loading 1`] = `
|
||||
<p>
|
||||
v
|
||||
</p>
|
||||
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@ -446,8 +438,6 @@ exports[`HoverOverlay multiple hovers present 1`] = `
|
||||
<p>
|
||||
v
|
||||
</p>
|
||||
|
||||
|
||||
</span>
|
||||
<hr />
|
||||
<span
|
||||
@ -457,8 +447,6 @@ exports[`HoverOverlay multiple hovers present 1`] = `
|
||||
<p>
|
||||
v2
|
||||
</p>
|
||||
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@ -19,8 +19,6 @@ exports[`Notices shows notices for location 1`] = `
|
||||
<p>
|
||||
a
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -37,8 +35,6 @@ exports[`Notices shows notices for location 1`] = `
|
||||
<p>
|
||||
a
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@ -171,6 +171,7 @@
|
||||
"@types/d3-shape": "^1.3.1",
|
||||
"@types/d3-time-format": "3.0.0",
|
||||
"@types/d3-voronoi": "^1.1.9",
|
||||
"@types/dompurify": "^3.0.0",
|
||||
"@types/escape-html": "^1.0.1",
|
||||
"@types/express": "4.17.11",
|
||||
"@types/glob": "7.1.3",
|
||||
@ -208,7 +209,6 @@
|
||||
"@types/recharts": "1.8.23",
|
||||
"@types/resize-observer-browser": "0.1.4",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/sanitize-html": "2.3.0",
|
||||
"@types/semver": "7.3.1",
|
||||
"@types/shelljs": "0.8.8",
|
||||
"@types/signale": "1.4.1",
|
||||
@ -414,6 +414,7 @@
|
||||
"d3-voronoi": "^1.1.2",
|
||||
"date-fns": "^2.16.1",
|
||||
"delay": "^4.4.1",
|
||||
"dompurify": "^3.0.1",
|
||||
"downshift": "^3.4.8",
|
||||
"escape-html": "^1.0.3",
|
||||
"eventsource": "1.1.1",
|
||||
@ -470,7 +471,6 @@
|
||||
"regexpp": "^3.1.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"rxjs": "^6.6.3",
|
||||
"sanitize-html": "^2.0.0",
|
||||
"semver": "^7.3.2",
|
||||
"stopword": "^2.0.7",
|
||||
"stream-http": "^3.2.0",
|
||||
|
||||
100
pnpm-lock.yaml
100
pnpm-lock.yaml
@ -131,6 +131,7 @@ importers:
|
||||
'@types/d3-shape': ^1.3.1
|
||||
'@types/d3-time-format': 3.0.0
|
||||
'@types/d3-voronoi': ^1.1.9
|
||||
'@types/dompurify': ^3.0.0
|
||||
'@types/escape-html': ^1.0.1
|
||||
'@types/express': 4.17.11
|
||||
'@types/glob': 7.1.3
|
||||
@ -168,7 +169,6 @@ importers:
|
||||
'@types/recharts': 1.8.23
|
||||
'@types/resize-observer-browser': 0.1.4
|
||||
'@types/rimraf': ^3.0.2
|
||||
'@types/sanitize-html': 2.3.0
|
||||
'@types/semver': 7.3.1
|
||||
'@types/shelljs': 0.8.8
|
||||
'@types/signale': 1.4.1
|
||||
@ -241,6 +241,7 @@ importers:
|
||||
d3-voronoi: ^1.1.2
|
||||
date-fns: ^2.16.1
|
||||
delay: ^4.4.1
|
||||
dompurify: ^3.0.1
|
||||
downshift: ^3.4.8
|
||||
envalid: ^7.3.1
|
||||
esbuild: ^0.17.14
|
||||
@ -351,7 +352,6 @@ importers:
|
||||
resize-observer-polyfill: ^1.5.1
|
||||
rimraf: ^3.0.2
|
||||
rxjs: ^6.6.3
|
||||
sanitize-html: ^2.0.0
|
||||
sass: ^1.32.4
|
||||
sass-loader: ^13.2.0
|
||||
semver: ^7.3.2
|
||||
@ -480,6 +480,7 @@ importers:
|
||||
d3-voronoi: 1.1.4
|
||||
date-fns: 2.29.3
|
||||
delay: 4.4.1
|
||||
dompurify: 3.0.1
|
||||
downshift: 3.4.8_react@18.1.0
|
||||
escape-html: 1.0.3
|
||||
eventsource: 1.1.1
|
||||
@ -536,7 +537,6 @@ importers:
|
||||
regexpp: 3.2.0
|
||||
resize-observer-polyfill: 1.5.1
|
||||
rxjs: 6.6.7
|
||||
sanitize-html: 2.3.3
|
||||
semver: 7.3.8
|
||||
stopword: 2.0.7
|
||||
stream-http: 3.2.0
|
||||
@ -636,6 +636,7 @@ importers:
|
||||
'@types/d3-shape': 1.3.2
|
||||
'@types/d3-time-format': 3.0.0
|
||||
'@types/d3-voronoi': 1.1.9
|
||||
'@types/dompurify': 3.0.0
|
||||
'@types/escape-html': 1.0.1
|
||||
'@types/express': 4.17.11
|
||||
'@types/glob': 7.1.3
|
||||
@ -673,7 +674,6 @@ importers:
|
||||
'@types/recharts': 1.8.23
|
||||
'@types/resize-observer-browser': 0.1.4
|
||||
'@types/rimraf': 3.0.2
|
||||
'@types/sanitize-html': 2.3.0
|
||||
'@types/semver': 7.3.1
|
||||
'@types/shelljs': 0.8.8
|
||||
'@types/signale': 1.4.1
|
||||
@ -924,7 +924,10 @@ importers:
|
||||
openai: 3.2.1
|
||||
|
||||
client/cody-shared:
|
||||
specifiers: {}
|
||||
specifiers:
|
||||
'@sourcegraph/common': workspace:*
|
||||
dependencies:
|
||||
'@sourcegraph/common': link:../common
|
||||
|
||||
client/common:
|
||||
specifiers:
|
||||
@ -3196,7 +3199,6 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm64/0.16.17:
|
||||
@ -3222,7 +3224,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-x64/0.16.17:
|
||||
@ -3248,7 +3249,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-arm64/0.16.17:
|
||||
@ -3274,7 +3274,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-x64/0.16.17:
|
||||
@ -3300,7 +3299,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-arm64/0.16.17:
|
||||
@ -3326,7 +3324,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-x64/0.16.17:
|
||||
@ -3352,7 +3349,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm/0.16.17:
|
||||
@ -3378,7 +3374,6 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm64/0.16.17:
|
||||
@ -3404,7 +3399,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ia32/0.16.17:
|
||||
@ -3430,7 +3424,6 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64/0.16.17:
|
||||
@ -3456,7 +3449,6 @@ packages:
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-mips64el/0.16.17:
|
||||
@ -3482,7 +3474,6 @@ packages:
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ppc64/0.16.17:
|
||||
@ -3508,7 +3499,6 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-riscv64/0.16.17:
|
||||
@ -3534,7 +3524,6 @@ packages:
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-s390x/0.16.17:
|
||||
@ -3560,7 +3549,6 @@ packages:
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-x64/0.16.17:
|
||||
@ -3586,7 +3574,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-x64/0.16.17:
|
||||
@ -3612,7 +3599,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-x64/0.16.17:
|
||||
@ -3638,7 +3624,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/sunos-x64/0.16.17:
|
||||
@ -3664,7 +3649,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-arm64/0.16.17:
|
||||
@ -3690,7 +3674,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-ia32/0.16.17:
|
||||
@ -3716,7 +3699,6 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-x64/0.16.17:
|
||||
@ -3742,7 +3724,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@eslint/eslintrc/1.4.1:
|
||||
@ -4835,8 +4816,8 @@ packages:
|
||||
resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
dependencies:
|
||||
'@types/istanbul-lib-coverage': 2.0.1
|
||||
'@types/istanbul-reports': 3.0.0
|
||||
'@types/istanbul-lib-coverage': 2.0.4
|
||||
'@types/istanbul-reports': 3.0.1
|
||||
'@types/node': 13.13.5
|
||||
'@types/yargs': 16.0.4
|
||||
chalk: 4.1.2
|
||||
@ -8854,6 +8835,12 @@ packages:
|
||||
/@types/d3-voronoi/1.1.9:
|
||||
resolution: {integrity: sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ==}
|
||||
|
||||
/@types/dompurify/3.0.0:
|
||||
resolution: {integrity: sha512-EcSqmgm/xJwH8CcJPy9AHNypp/j58CYga3nWdl93/wLxX6OH+rSD3aAj75NQazcZd1YKHJ/pjNZ9qmgVajggwQ==}
|
||||
dependencies:
|
||||
'@types/trusted-types': 2.0.3
|
||||
dev: true
|
||||
|
||||
/@types/escape-html/1.0.1:
|
||||
resolution: {integrity: sha512-4mI1FuUUZiuT95fSVqvZxp/ssQK9zsa86S43h9x3zPOSU9BBJ+BfDkXwuaU7BfsD+e7U0/cUUfJFk3iW2M4okA==}
|
||||
dev: true
|
||||
@ -8999,7 +8986,6 @@ packages:
|
||||
|
||||
/@types/istanbul-lib-coverage/2.0.4:
|
||||
resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
|
||||
dev: false
|
||||
|
||||
/@types/istanbul-lib-report/1.1.1:
|
||||
resolution: {integrity: sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==}
|
||||
@ -9010,7 +8996,6 @@ packages:
|
||||
resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==}
|
||||
dependencies:
|
||||
'@types/istanbul-lib-coverage': 2.0.4
|
||||
dev: false
|
||||
|
||||
/@types/istanbul-reports/3.0.0:
|
||||
resolution: {integrity: sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==}
|
||||
@ -9021,7 +9006,6 @@ packages:
|
||||
resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==}
|
||||
dependencies:
|
||||
'@types/istanbul-lib-report': 3.0.0
|
||||
dev: false
|
||||
|
||||
/@types/jest-image-snapshot/4.3.0:
|
||||
resolution: {integrity: sha512-gb6zF1ICfvzBsQYMTq2qFhhiI46Cab/t5PtLK4Z3mpbyQoyKI2HgCFDi71iE7rjs6TrIPnsf2GXw+mnGvZSgrA==}
|
||||
@ -9341,12 +9325,6 @@ packages:
|
||||
'@types/node': 13.13.5
|
||||
dev: true
|
||||
|
||||
/@types/sanitize-html/2.3.0:
|
||||
resolution: {integrity: sha512-q+Xg5t8Yn0KeomXMyVMoxtKyvh2u1ywkPqFlMy/5luF8D+DN+HhFN9pesJ6BsuoLuDCukR8p922KkCZnkTHOpg==}
|
||||
dependencies:
|
||||
htmlparser2: 6.1.0
|
||||
dev: true
|
||||
|
||||
/@types/sass/1.16.1:
|
||||
resolution: {integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ==}
|
||||
dependencies:
|
||||
@ -9478,6 +9456,10 @@ packages:
|
||||
/@types/tough-cookie/2.3.5:
|
||||
resolution: {integrity: sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==}
|
||||
|
||||
/@types/trusted-types/2.0.3:
|
||||
resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==}
|
||||
dev: true
|
||||
|
||||
/@types/undertaker-registry/1.0.1:
|
||||
resolution: {integrity: sha512-Z4TYuEKn9+RbNVk1Ll2SS4x1JeLHecolIbM/a8gveaHsW0Hr+RQMraZACwTO2VD7JvepgA6UO1A1VrbktQrIbQ==}
|
||||
dev: true
|
||||
@ -9566,7 +9548,6 @@ packages:
|
||||
|
||||
/@types/yargs-parser/21.0.0:
|
||||
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
||||
dev: false
|
||||
|
||||
/@types/yargs/15.0.3:
|
||||
resolution: {integrity: sha512-XCMQRK6kfpNBixHLyHUsGmXrpEmFFxzMrcnSXFMziHd8CoNJo8l16FkHyQq4x+xbM7E2XL83/O78OD8u+iZTdQ==}
|
||||
@ -9576,7 +9557,7 @@ packages:
|
||||
/@types/yargs/16.0.4:
|
||||
resolution: {integrity: sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==}
|
||||
dependencies:
|
||||
'@types/yargs-parser': 13.0.0
|
||||
'@types/yargs-parser': 21.0.0
|
||||
|
||||
/@types/yargs/17.0.16:
|
||||
resolution: {integrity: sha512-Mh3OP0oh8X7O7F9m5AplC+XHYLBWuPKNkGVD3gIZFLFebBnuFI2Nz5Sf8WLvwGxECJ8YjifQvFdh79ubODkdug==}
|
||||
@ -13016,7 +12997,7 @@ packages:
|
||||
postcss-modules-values: 4.0.0_postcss@8.4.21
|
||||
postcss-value-parser: 4.2.0
|
||||
semver: 7.3.8
|
||||
webpack: 5.75.0_pdcrf7mb3dfag2zju4x4octu4a
|
||||
webpack: 5.75.0_esbuild@0.17.8
|
||||
|
||||
/css-minimizer-webpack-plugin/4.2.2_zj7shrtzhjuywytipisjis56au:
|
||||
resolution: {integrity: sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA==}
|
||||
@ -14255,6 +14236,7 @@ packages:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.2.0
|
||||
entities: 2.1.0
|
||||
dev: true
|
||||
|
||||
/dom-serializer/2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
@ -14273,6 +14255,7 @@ packages:
|
||||
|
||||
/domelementtype/2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
dev: true
|
||||
|
||||
/domexception/2.0.1:
|
||||
resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==}
|
||||
@ -14293,6 +14276,7 @@ packages:
|
||||
engines: {node: '>= 4'}
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
dev: true
|
||||
|
||||
/domhandler/5.0.3:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
@ -14305,6 +14289,10 @@ packages:
|
||||
resolution: {integrity: sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==}
|
||||
dev: true
|
||||
|
||||
/dompurify/3.0.1:
|
||||
resolution: {integrity: sha512-60tsgvPKwItxZZdfLmamp0MTcecCta3avOhsLgPZ0qcWt96OasFfhkeIRbJ6br5i0fQawT1/RBGB5L58/Jpwuw==}
|
||||
dev: false
|
||||
|
||||
/domutils/1.7.0:
|
||||
resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
|
||||
dependencies:
|
||||
@ -14318,6 +14306,7 @@ packages:
|
||||
dom-serializer: 1.3.2
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.2.0
|
||||
dev: true
|
||||
|
||||
/domutils/3.0.1:
|
||||
resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==}
|
||||
@ -14793,7 +14782,6 @@ packages:
|
||||
'@esbuild/win32-arm64': 0.17.8
|
||||
'@esbuild/win32-ia32': 0.17.8
|
||||
'@esbuild/win32-x64': 0.17.8
|
||||
dev: false
|
||||
|
||||
/escalade/3.1.1:
|
||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||
@ -16083,7 +16071,7 @@ packages:
|
||||
/fs-extra/0.30.0:
|
||||
resolution: {integrity: sha512-UvSPKyhMn6LEd/WpUaV9C9t3zATuqoqfWc3QdPhPLb58prN9tqYPlPWi8Krxi44loBoUzlobqZ3+8tGpxxSzwA==}
|
||||
dependencies:
|
||||
graceful-fs: 4.2.10
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 2.4.0
|
||||
klaw: 1.3.1
|
||||
path-is-absolute: 1.0.1
|
||||
@ -17348,6 +17336,7 @@ packages:
|
||||
domhandler: 4.2.0
|
||||
domutils: 2.8.0
|
||||
entities: 2.1.0
|
||||
dev: true
|
||||
|
||||
/htmlparser2/8.0.1:
|
||||
resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==}
|
||||
@ -18142,6 +18131,7 @@ packages:
|
||||
/is-plain-object/5.0.0:
|
||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/is-potential-custom-element-name/1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
@ -22266,10 +22256,6 @@ packages:
|
||||
semver: 5.7.1
|
||||
dev: true
|
||||
|
||||
/parse-srcset/1.0.2:
|
||||
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
|
||||
dev: false
|
||||
|
||||
/parse5-htmlparser2-tree-adapter/7.0.0:
|
||||
resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
|
||||
dependencies:
|
||||
@ -22729,7 +22715,7 @@ packages:
|
||||
klona: 2.0.5
|
||||
postcss: 8.4.21
|
||||
semver: 7.3.8
|
||||
webpack: 5.75.0_pdcrf7mb3dfag2zju4x4octu4a
|
||||
webpack: 5.75.0_esbuild@0.17.8
|
||||
|
||||
/postcss-media-query-parser/0.2.3:
|
||||
resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==}
|
||||
@ -24930,18 +24916,6 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/sanitize-html/2.3.3:
|
||||
resolution: {integrity: sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA==}
|
||||
dependencies:
|
||||
deepmerge: 4.2.2
|
||||
escape-string-regexp: 4.0.0
|
||||
htmlparser2: 6.1.0
|
||||
is-plain-object: 5.0.0
|
||||
klona: 2.0.5
|
||||
parse-srcset: 1.0.2
|
||||
postcss: 8.4.21
|
||||
dev: false
|
||||
|
||||
/sass-loader/13.2.0_sass@1.32.4+webpack@5.75.0:
|
||||
resolution: {integrity: sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg==}
|
||||
engines: {node: '>= 14.15.0'}
|
||||
@ -24964,7 +24938,7 @@ packages:
|
||||
klona: 2.0.5
|
||||
neo-async: 2.6.2
|
||||
sass: 1.32.4
|
||||
webpack: 5.75.0_pdcrf7mb3dfag2zju4x4octu4a
|
||||
webpack: 5.75.0_esbuild@0.17.8
|
||||
|
||||
/sass/1.32.4:
|
||||
resolution: {integrity: sha512-N0BT0PI/t3+gD8jKa83zJJUb7ssfQnRRfqN+GIErokW6U4guBpfYl8qYB+OFLEho+QvnV5ZH1R9qhUC/Z2Ch9w==}
|
||||
@ -26112,7 +26086,7 @@ packages:
|
||||
peerDependencies:
|
||||
webpack: ^5.0.0
|
||||
dependencies:
|
||||
webpack: 5.75.0_pdcrf7mb3dfag2zju4x4octu4a
|
||||
webpack: 5.75.0_esbuild@0.17.8
|
||||
|
||||
/style-mod/4.0.0:
|
||||
resolution: {integrity: sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==}
|
||||
@ -26657,7 +26631,6 @@ packages:
|
||||
serialize-javascript: 6.0.0
|
||||
terser: 5.16.1
|
||||
webpack: 5.75.0_esbuild@0.17.8
|
||||
dev: false
|
||||
|
||||
/terser-webpack-plugin/5.3.6_zj7shrtzhjuywytipisjis56au:
|
||||
resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==}
|
||||
@ -28475,7 +28448,6 @@ packages:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
- uglify-js
|
||||
dev: false
|
||||
|
||||
/webpack/5.75.0_pdcrf7mb3dfag2zju4x4octu4a:
|
||||
resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==}
|
||||
|
||||
@ -1102,7 +1102,6 @@ Yarn,rxjs,6.6.7,Apache 2.0,https://github.com/ReactiveX/RxJS,Approved
|
||||
Yarn,safe-buffer,5.1.2,MIT,https://github.com/feross/safe-buffer,Approved
|
||||
Yarn,safe-buffer,5.2.1,MIT,https://github.com/feross/safe-buffer,Approved
|
||||
Yarn,safer-buffer,2.1.2,MIT,https://github.com/ChALkeR,Approved
|
||||
Yarn,sanitize-html,2.3.3,MIT,"",Approved
|
||||
Yarn,scheduler,0.22.0,MIT,https://reactjs.org/,Approved
|
||||
Yarn,schema-utils,2.7.0,MIT,https://github.com/webpack/schema-utils,Approved
|
||||
Yarn,schema-utils,3.1.0,MIT,https://github.com/webpack/schema-utils,Approved
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user