From 0ff5d176fb31dd92a58d15ff181edd1532f0ae52 Mon Sep 17 00:00:00 2001 From: Sergei Garin Date: Mon, 16 Dec 2024 21:47:09 +0400 Subject: [PATCH 1/7] Load images from local backend --- app/common/src/text/english.json | 5 ++++ app/gui/index.html | 1 + .../MarkdownViewer/MarkdownViewer.tsx | 29 ++++++++++++++----- .../src/dashboard/services/RemoteBackend.ts | 8 +++-- app/ide-desktop/client/src/projectManager.ts | 2 +- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/app/common/src/text/english.json b/app/common/src/text/english.json index a89c0135bc24..40e61d25a5b8 100644 --- a/app/common/src/text/english.json +++ b/app/common/src/text/english.json @@ -1,6 +1,10 @@ { "submit": "Submit", "retry": "Retry", + + "arbitraryFetchError": "An error occurred while fetching data", + "arbitraryFetchImageError": "An error occurred while fetching image", + "createFolderError": "Could not create new folder", "createProjectError": "Could not create new project", "createDatalinkError": "Could not create new Datalink", @@ -174,6 +178,7 @@ "getCustomerPortalUrlBackendError": "Could not get customer portal URL", "duplicateLabelError": "This label already exists.", "emptyStringError": "This value must not be empty.", + "resolveProjectAssetPath": "Could not get asset", "directoryAssetType": "folder", "directoryDoesNotExistError": "Unable to find directory. Does it exist?", diff --git a/app/gui/index.html b/app/gui/index.html index 2fd49ff74630..c6f2d13198a3 100644 --- a/app/gui/index.html +++ b/app/gui/index.html @@ -26,6 +26,7 @@ style-src 'self' 'unsafe-inline' data: https://*; connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://* wss://*; worker-src 'self' blob:; + object-src 'self' blob: enso: https://*; img-src 'self' blob: enso: data: https://*; font-src 'self' data: https://*" /> diff --git a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx index 96970d66f50e..0338d964ae70 100644 --- a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx +++ b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx @@ -4,20 +4,22 @@ import { useSuspenseQuery } from '@tanstack/react-query' import type { RendererObject } from 'marked' import { marked } from 'marked' import { useMemo } from 'react' +import { useText } from '../../providers/TextProvider' import { BUTTON_STYLES, TEXT_STYLE, type TestIdProps } from '../AriaComponents' /** Props for a {@link MarkdownViewer}. */ export interface MarkdownViewerProps extends TestIdProps { /** Markdown markup to parse and display. */ readonly text: string - readonly imgUrlResolver: (relativePath: string) => Promise + readonly imgUrlResolver: (relativePath: string) => Promise readonly renderer?: RendererObject } const defaultRenderer: RendererObject = { /** The renderer for headings. */ heading({ depth, tokens }) { - return `${this.parser.parseInline(tokens)}` + const variant = depth === 1 ? 'h1' : 'subtitle' + return `${this.parser.parseInline(tokens)}` }, /** The renderer for paragraphs. */ paragraph({ tokens }) { @@ -36,8 +38,14 @@ const defaultRenderer: RendererObject = { return `${this.parser.parseInline(tokens)}` }, /** The renderer for images. */ - image({ href, title }) { - return `${title}` + image({ href, title, raw, text }) { + const alt = title ?? '' + + return ` + + ${text} + + ` }, /** The renderer for code. */ code({ text }) { @@ -58,23 +66,30 @@ const defaultRenderer: RendererObject = { export function MarkdownViewer(props: MarkdownViewerProps) { const { text, imgUrlResolver, renderer = defaultRenderer, testId } = props + const { getText } = useText() + const markedInstance = useMemo( - () => marked.use({ renderer: Object.assign({}, defaultRenderer, renderer), async: true }), + () => marked.use({ renderer: Object.assign({}, defaultRenderer, renderer) }), [renderer], ) const { data: markdownToHtml } = useSuspenseQuery({ - queryKey: ['markdownToHtml', { text }], + queryKey: ['markdownToHtml', { text, imgUrlResolver }], queryFn: () => markedInstance.parse(text, { async: true, walkTokens: async (token) => { if (token.type === 'image' && 'href' in token && typeof token.href === 'string') { - token.href = await imgUrlResolver(token.href) + const href = token.href + + token.raw = href + token.href = await imgUrlResolver(href).catch(() => null) + token.text = getText('arbitraryFetchImageError') } }, }), }) + return (
{ - const response = await this.get( + const response = await this.get( remoteBackendPaths.getProjectAssetPath(projectId, relativePath), ) if (!responseIsSuccessful(response)) { - return Promise.reject(new Error('Not implemented.')) + return await this.throw(response, 'resolveProjectAssetPath') } else { - return await response.text() + const blob = await response.blob() + const objUrl = URL.createObjectURL(blob) + return objUrl } } diff --git a/app/ide-desktop/client/src/projectManager.ts b/app/ide-desktop/client/src/projectManager.ts index 6b83e09e8b8b..00801d1cdb02 100644 --- a/app/ide-desktop/client/src/projectManager.ts +++ b/app/ide-desktop/client/src/projectManager.ts @@ -103,7 +103,7 @@ export async function version(args: config.Args) { } /** - * Handle requests to the `enso-project` protocol. + * Handle requests to the `enso://` protocol. * * The protocol is used to fetch project assets from the backend. * If a given path is not inside a project, the request is rejected with a 403 error. From 1784d21b9d1148f1768da043b51075d688a2adde Mon Sep 17 00:00:00 2001 From: Sergei Garin Date: Mon, 16 Dec 2024 22:02:39 +0400 Subject: [PATCH 2/7] Fixes and adjustments --- app/common/src/text/english.json | 2 +- .../MarkdownViewer/MarkdownViewer.tsx | 65 +++---------------- .../MarkdownViewer/defaultRenderer.ts | 48 ++++++++++++++ .../src/dashboard/services/RemoteBackend.ts | 5 +- 4 files changed, 60 insertions(+), 60 deletions(-) create mode 100644 app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts diff --git a/app/common/src/text/english.json b/app/common/src/text/english.json index 40e61d25a5b8..a1951c9af08e 100644 --- a/app/common/src/text/english.json +++ b/app/common/src/text/english.json @@ -178,7 +178,7 @@ "getCustomerPortalUrlBackendError": "Could not get customer portal URL", "duplicateLabelError": "This label already exists.", "emptyStringError": "This value must not be empty.", - "resolveProjectAssetPath": "Could not get asset", + "resolveProjectAssetPathBackendError": "Could not get asset", "directoryAssetType": "folder", "directoryDoesNotExistError": "Unable to find directory. Does it exist?", diff --git a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx index 0338d964ae70..4a85f33a5a34 100644 --- a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx +++ b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx @@ -1,11 +1,11 @@ -/** @file A WYSIWYG editor using Lexical.js. */ +/** @file A Markdown viewer component. */ +import { useText } from '#/providers/TextProvider' import { useSuspenseQuery } from '@tanstack/react-query' import type { RendererObject } from 'marked' import { marked } from 'marked' -import { useMemo } from 'react' -import { useText } from '../../providers/TextProvider' -import { BUTTON_STYLES, TEXT_STYLE, type TestIdProps } from '../AriaComponents' +import { type TestIdProps } from '../AriaComponents' +import { defaultRenderer } from './defaultRenderer' /** Props for a {@link MarkdownViewer}. */ export interface MarkdownViewerProps extends TestIdProps { @@ -15,50 +15,6 @@ export interface MarkdownViewerProps extends TestIdProps { readonly renderer?: RendererObject } -const defaultRenderer: RendererObject = { - /** The renderer for headings. */ - heading({ depth, tokens }) { - const variant = depth === 1 ? 'h1' : 'subtitle' - return `${this.parser.parseInline(tokens)}` - }, - /** The renderer for paragraphs. */ - paragraph({ tokens }) { - return `

${this.parser.parseInline(tokens)}

` - }, - /** The renderer for list items. */ - listitem({ tokens }) { - return `
  • ${this.parser.parseInline(tokens)}
  • ` - }, - /** The renderer for lists. */ - list({ items }) { - return `
      ${items.map((item) => this.listitem(item)).join('\n')}
    ` - }, - /** The renderer for links. */ - link({ href, tokens }) { - return `${this.parser.parseInline(tokens)}` - }, - /** The renderer for images. */ - image({ href, title, raw, text }) { - const alt = title ?? '' - - return ` - - ${text} - - ` - }, - /** The renderer for code. */ - code({ text }) { - return ` -
    ${text}
    -
    ` - }, - /** The renderer for blockquotes. */ - blockquote({ tokens }) { - return `
    ${this.parser.parse(tokens)}
    ` - }, -} - /** * Markdown viewer component. * Parses markdown passed in as a `text` prop into HTML and displays it. @@ -68,22 +24,19 @@ export function MarkdownViewer(props: MarkdownViewerProps) { const { getText } = useText() - const markedInstance = useMemo( - () => marked.use({ renderer: Object.assign({}, defaultRenderer, renderer) }), - [renderer], - ) + const markedInstance = marked.use({ renderer: Object.assign({}, defaultRenderer, renderer) }) const { data: markdownToHtml } = useSuspenseQuery({ - queryKey: ['markdownToHtml', { text, imgUrlResolver }], - queryFn: () => - markedInstance.parse(text, { + queryKey: ['markdownToHtml', { text, imgUrlResolver, markedInstance }] as const, + queryFn: ({ queryKey: [, args] }) => + args.markedInstance.parse(args.text, { async: true, walkTokens: async (token) => { if (token.type === 'image' && 'href' in token && typeof token.href === 'string') { const href = token.href token.raw = href - token.href = await imgUrlResolver(href).catch(() => null) + token.href = await args.imgUrlResolver(href).catch(() => null) token.text = getText('arbitraryFetchImageError') } }, diff --git a/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts b/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts new file mode 100644 index 000000000000..032da7bf4f5d --- /dev/null +++ b/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts @@ -0,0 +1,48 @@ +/** @file The default renderer for Markdown. */ +import type { RendererObject } from 'marked' +import { BUTTON_STYLES, TEXT_STYLE } from '../AriaComponents' + +/** The default renderer for Markdown. */ +export const defaultRenderer: RendererObject = { + /** The renderer for headings. */ + heading({ depth, tokens }) { + const variant = depth === 1 ? 'h1' : 'subtitle' + return `${this.parser.parseInline(tokens)}` + }, + /** The renderer for paragraphs. */ + paragraph({ tokens }) { + return `

    ${this.parser.parseInline(tokens)}

    ` + }, + /** The renderer for list items. */ + listitem({ tokens }) { + return `
  • ${this.parser.parseInline(tokens)}
  • ` + }, + /** The renderer for lists. */ + list({ items }) { + return `
      ${items.map((item) => this.listitem(item)).join('\n')}
    ` + }, + /** The renderer for links. */ + link({ href, tokens }) { + return `${this.parser.parseInline(tokens)}` + }, + /** The renderer for images. */ + image({ href, title, raw, text }) { + const alt = title ?? '' + + return ` + + ${text} + + ` + }, + /** The renderer for code. */ + code({ text }) { + return ` +
    ${text}
    +
    ` + }, + /** The renderer for blockquotes. */ + blockquote({ tokens }) { + return `
    ${this.parser.parse(tokens)}
    ` + }, +} diff --git a/app/gui/src/dashboard/services/RemoteBackend.ts b/app/gui/src/dashboard/services/RemoteBackend.ts index 6e28879037aa..817732cb1785 100644 --- a/app/gui/src/dashboard/services/RemoteBackend.ts +++ b/app/gui/src/dashboard/services/RemoteBackend.ts @@ -1273,11 +1273,10 @@ export default class RemoteBackend extends Backend { ) if (!responseIsSuccessful(response)) { - return await this.throw(response, 'resolveProjectAssetPath') + return await this.throw(response, 'resolveProjectAssetPathBackendError') } else { const blob = await response.blob() - const objUrl = URL.createObjectURL(blob) - return objUrl + return URL.createObjectURL(blob) } } From 6492f7d9bcf4602b70875354a9adca733ac3244f Mon Sep 17 00:00:00 2001 From: Sergei Garin Date: Tue, 17 Dec 2024 01:20:44 +0400 Subject: [PATCH 3/7] Fix types --- .../src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx index 4a85f33a5a34..709d617623b8 100644 --- a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx +++ b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx @@ -11,7 +11,7 @@ import { defaultRenderer } from './defaultRenderer' export interface MarkdownViewerProps extends TestIdProps { /** Markdown markup to parse and display. */ readonly text: string - readonly imgUrlResolver: (relativePath: string) => Promise + readonly imgUrlResolver: (relativePath: string) => Promise readonly renderer?: RendererObject } From 5c8beb3d9f6bda1e80f7b7241d311962dfd8aa57 Mon Sep 17 00:00:00 2001 From: Sergei Garin Date: Tue, 17 Dec 2024 01:22:12 +0400 Subject: [PATCH 4/7] FIx prettier --- tools/performance/engine-benchmarks/README.md | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tools/performance/engine-benchmarks/README.md b/tools/performance/engine-benchmarks/README.md index a839647dbb45..0363635f7da8 100644 --- a/tools/performance/engine-benchmarks/README.md +++ b/tools/performance/engine-benchmarks/README.md @@ -28,17 +28,26 @@ One can also download only a CSV file representing all the selected benchmark results with `bench_download.py --create-csv`. ## Contribute + Run local tests with: + ```bash python -m unittest --verbose bench_tool/test*.py ``` ## Relation to GH Actions -The `bench_download.py` script is used in [Benchmarks Upload](https://github.com/enso-org/enso/actions/workflows/bench-upload.yml) -GH Action to download the benchmarks generated by the [Benchmark Engine](https://github.com/enso-org/enso/actions/workflows/engine-benchmark.yml) -and [Benchmark Standard Libraries](https://github.com/enso-org/enso/actions/workflows/std-libs-benchmark.yml) GH Actions. -The `Benchmarks Upload` action is triggered by the `engine-benchmark.yml` and `std-libs-benchmark.yml` actions. - -The results from the benchmarks are gathered from the GH artifacts associated with corresponding workflow runs, and -save as JSON files inside https://github.com/enso-org/engine-benchmark-results repo inside its -[cache](https://github.com/enso-org/engine-benchmark-results/tree/main/cache) directory. + +The `bench_download.py` script is used in +[Benchmarks Upload](https://github.com/enso-org/enso/actions/workflows/bench-upload.yml) +GH Action to download the benchmarks generated by the +[Benchmark Engine](https://github.com/enso-org/enso/actions/workflows/engine-benchmark.yml) +and +[Benchmark Standard Libraries](https://github.com/enso-org/enso/actions/workflows/std-libs-benchmark.yml) +GH Actions. The `Benchmarks Upload` action is triggered by the +`engine-benchmark.yml` and `std-libs-benchmark.yml` actions. + +The results from the benchmarks are gathered from the GH artifacts associated +with corresponding workflow runs, and save as JSON files inside +https://github.com/enso-org/engine-benchmark-results repo inside its +[cache](https://github.com/enso-org/engine-benchmark-results/tree/main/cache) +directory. From 5ca5d7a20c103051e824f956cb44ce5d8cf84e45 Mon Sep 17 00:00:00 2001 From: Sergei Garin Date: Tue, 17 Dec 2024 13:43:07 +0400 Subject: [PATCH 5/7] Fix tests --- .../integration-test/dashboard/actions/api.ts | 29 +++++++++++------- .../dashboard/assetPanel.spec.ts | 23 +++++++++++++- .../dashboard/mock/example.png | Bin 0 -> 9033 bytes .../MarkdownViewer/MarkdownViewer.tsx | 12 +++++++- .../MarkdownViewer/defaultRenderer.ts | 6 ++-- 5 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 app/gui/integration-test/dashboard/mock/example.png diff --git a/app/gui/integration-test/dashboard/actions/api.ts b/app/gui/integration-test/dashboard/actions/api.ts index 9944c8bbc14f..09f7c15cbb5f 100644 --- a/app/gui/integration-test/dashboard/actions/api.ts +++ b/app/gui/integration-test/dashboard/actions/api.ts @@ -15,6 +15,7 @@ import * as actions from '.' import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' +import invariant from 'tiny-invariant' const __dirname = dirname(fileURLToPath(import.meta.url)) @@ -1129,17 +1130,23 @@ async function mockApiInternal({ page, setupAPI }: MockParams) { }) }) - await get(remoteBackendPaths.getProjectAssetPath(GLOB_PROJECT_ID, '*'), (route, request) => { - const maybeId = request.url().match(/[/]projects[/]([^?/]+)/)?.[1] - if (!maybeId) return - const projectId = backend.ProjectId(maybeId) - called('getProjectAsset', { projectId }) - return route.fulfill({ - // This is a mock SVG image. Just a square with a black background. - body: '/mock/svg.svg', - contentType: 'text/plain', - }) - }) + await get( + remoteBackendPaths.getProjectAssetPath(GLOB_PROJECT_ID, '*'), + async (route, request) => { + const maybeId = request.url().match(/[/]projects[/]([^?/]+)/)?.[1] + + invariant(maybeId, 'Unable to parse the ID provided') + + const projectId = backend.ProjectId(maybeId) + + called('getProjectAsset', { projectId }) + + return route.fulfill({ + // This is a mock SVG image. Just a square with a black background. + path: join(__dirname, '../mock/example.png'), + }) + }, + ) await page.route('mock/svg.svg', (route) => { return route.fulfill({ body: MOCK_SVG, contentType: 'image/svg+xml' }) diff --git a/app/gui/integration-test/dashboard/assetPanel.spec.ts b/app/gui/integration-test/dashboard/assetPanel.spec.ts index 9282cf573724..37e8ffd05102 100644 --- a/app/gui/integration-test/dashboard/assetPanel.spec.ts +++ b/app/gui/integration-test/dashboard/assetPanel.spec.ts @@ -5,7 +5,7 @@ import { EmailAddress, UserId } from '#/services/Backend' import { PermissionAction } from '#/utilities/permissions' -import { mockAllAndLogin } from './actions' +import { mockAllAndLogin, TEXT } from './actions' /** Find an asset panel. */ function locateAssetPanel(page: Page) { @@ -87,4 +87,25 @@ test('Asset Panel documentation view', ({ page }) => await expect(assetPanel.getByTestId('asset-panel-tab-panel-docs')).toBeVisible() await expect(assetPanel.getByTestId('asset-docs-content')).toBeVisible() await expect(assetPanel.getByTestId('asset-docs-content')).toHaveText(/Project Goal/) + await expect(assetPanel.getByText(TEXT.arbitraryFetchImageError)).not.toBeVisible() })) + +test('Assets Panel docs images', ({ page }) => { + return mockAllAndLogin({ + page, + setupAPI: (api) => { + api.addProject({}) + }, + }) + .do(() => {}) + .driveTable.clickRow(0) + .toggleDocsAssetPanel() + .withAssetPanel(async (assetPanel) => { + await expect(assetPanel.getByTestId('asset-docs-content')).toBeVisible() + + for (const image of await assetPanel.getByRole('img').all()) { + await expect(image).toBeVisible() + await expect(image).toHaveJSProperty('complete', true) + } + }) +}) diff --git a/app/gui/integration-test/dashboard/mock/example.png b/app/gui/integration-test/dashboard/mock/example.png new file mode 100644 index 0000000000000000000000000000000000000000..b4d6d8b3cb9ce9011614edf353a4ede9919f057e GIT binary patch literal 9033 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 literal 0 HcmV?d00001 diff --git a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx index 709d617623b8..ebd199958086 100644 --- a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx +++ b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx @@ -1,5 +1,6 @@ /** @file A Markdown viewer component. */ +import { useLogger } from '#/providers/LoggerProvider' import { useText } from '#/providers/TextProvider' import { useSuspenseQuery } from '@tanstack/react-query' import type { RendererObject } from 'marked' @@ -23,6 +24,7 @@ export function MarkdownViewer(props: MarkdownViewerProps) { const { text, imgUrlResolver, renderer = defaultRenderer, testId } = props const { getText } = useText() + const logger = useLogger() const markedInstance = marked.use({ renderer: Object.assign({}, defaultRenderer, renderer) }) @@ -36,7 +38,15 @@ export function MarkdownViewer(props: MarkdownViewerProps) { const href = token.href token.raw = href - token.href = await args.imgUrlResolver(href).catch(() => null) + token.href = await args + .imgUrlResolver(href) + .then((url) => { + return url + }) + .catch((error) => { + logger.error(error) + return null + }) token.text = getText('arbitraryFetchImageError') } }, diff --git a/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts b/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts index 032da7bf4f5d..ac2a909e2987 100644 --- a/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts +++ b/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts @@ -26,13 +26,11 @@ export const defaultRenderer: RendererObject = { return `${this.parser.parseInline(tokens)}` }, /** The renderer for images. */ - image({ href, title, raw, text }) { + image({ href, title, raw }) { const alt = title ?? '' return ` - - ${text} - + ${alt} ` }, /** The renderer for code. */ From 2b29485b587575a58675e47f961a442f97d2d8fb Mon Sep 17 00:00:00 2001 From: Sergei Garin Date: Tue, 17 Dec 2024 13:48:18 +0400 Subject: [PATCH 6/7] Remove unused CSP --- app/gui/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/app/gui/index.html b/app/gui/index.html index c6f2d13198a3..2fd49ff74630 100644 --- a/app/gui/index.html +++ b/app/gui/index.html @@ -26,7 +26,6 @@ style-src 'self' 'unsafe-inline' data: https://*; connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://* wss://*; worker-src 'self' blob:; - object-src 'self' blob: enso: https://*; img-src 'self' blob: enso: data: https://*; font-src 'self' data: https://*" /> From 49fb5a9ea5a9eb19e8f1f004dea2efb855bf46a1 Mon Sep 17 00:00:00 2001 From: Sergei Garin Date: Tue, 17 Dec 2024 14:00:28 +0400 Subject: [PATCH 7/7] Adress issues --- app/common/src/text/english.json | 2 +- .../dashboard/components/MarkdownViewer/MarkdownViewer.tsx | 6 +++--- .../dashboard/components/MarkdownViewer/defaultRenderer.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/common/src/text/english.json b/app/common/src/text/english.json index a1951c9af08e..77d36c0eed46 100644 --- a/app/common/src/text/english.json +++ b/app/common/src/text/english.json @@ -3,7 +3,7 @@ "retry": "Retry", "arbitraryFetchError": "An error occurred while fetching data", - "arbitraryFetchImageError": "An error occurred while fetching image", + "arbitraryFetchImageError": "An error occurred while fetching an image", "createFolderError": "Could not create new folder", "createProjectError": "Could not create new project", diff --git a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx index ebd199958086..1531ff5e6974 100644 --- a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx +++ b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx @@ -6,7 +6,7 @@ import { useSuspenseQuery } from '@tanstack/react-query' import type { RendererObject } from 'marked' import { marked } from 'marked' import { type TestIdProps } from '../AriaComponents' -import { defaultRenderer } from './defaultRenderer' +import { DEFAULT_RENDERER } from './defaultRenderer' /** Props for a {@link MarkdownViewer}. */ export interface MarkdownViewerProps extends TestIdProps { @@ -21,12 +21,12 @@ export interface MarkdownViewerProps extends TestIdProps { * Parses markdown passed in as a `text` prop into HTML and displays it. */ export function MarkdownViewer(props: MarkdownViewerProps) { - const { text, imgUrlResolver, renderer = defaultRenderer, testId } = props + const { text, imgUrlResolver, renderer = {}, testId } = props const { getText } = useText() const logger = useLogger() - const markedInstance = marked.use({ renderer: Object.assign({}, defaultRenderer, renderer) }) + const markedInstance = marked.use({ renderer: Object.assign({}, DEFAULT_RENDERER, renderer) }) const { data: markdownToHtml } = useSuspenseQuery({ queryKey: ['markdownToHtml', { text, imgUrlResolver, markedInstance }] as const, diff --git a/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts b/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts index ac2a909e2987..e9b609d45c7d 100644 --- a/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts +++ b/app/gui/src/dashboard/components/MarkdownViewer/defaultRenderer.ts @@ -3,7 +3,7 @@ import type { RendererObject } from 'marked' import { BUTTON_STYLES, TEXT_STYLE } from '../AriaComponents' /** The default renderer for Markdown. */ -export const defaultRenderer: RendererObject = { +export const DEFAULT_RENDERER: Readonly = { /** The renderer for headings. */ heading({ depth, tokens }) { const variant = depth === 1 ? 'h1' : 'subtitle'