diff --git a/packages/stage-ui/package.json b/packages/stage-ui/package.json
index 98e8b390c..449677ae5 100644
--- a/packages/stage-ui/package.json
+++ b/packages/stage-ui/package.json
@@ -200,6 +200,7 @@
"@types/culori": "catalog:",
"@types/d3": "catalog:",
"@types/hast": "catalog:",
+ "@types/mdast": "catalog:",
"@types/splitpanes": "catalog:",
"@types/unist": "catalog:",
"@types/ws": "catalog:",
diff --git a/packages/stage-ui/src/components/markdown/markdown-renderer.browser.test.ts b/packages/stage-ui/src/components/markdown/markdown-renderer.browser.test.ts
index 058af1817..7107cec8b 100644
--- a/packages/stage-ui/src/components/markdown/markdown-renderer.browser.test.ts
+++ b/packages/stage-ui/src/components/markdown/markdown-renderer.browser.test.ts
@@ -4,16 +4,16 @@ import { defineComponent, onMounted, ref, useTemplateRef } from 'vue'
import MarkdownRenderer from './markdown-renderer.vue'
-const markdown = `### Roof Leak in Server Room
-
-*Anime style virtual AI girl waking up in a server room and noticing water from the ceiling.*
-
-> **Sharing**: Sending a quick sketch to our chat history...`
-
const MarkdownMountProbe = defineComponent({
components: {
MarkdownRenderer,
},
+ props: {
+ markdown: {
+ type: String,
+ required: true,
+ },
+ },
emits: {
mountedHtml: (html: string) => typeof html === 'string',
},
@@ -25,7 +25,6 @@ const MarkdownMountProbe = defineComponent({
})
return {
- markdown,
root,
}
},
@@ -36,22 +35,26 @@ const MarkdownMountProbe = defineComponent({
`,
})
-const MarkdownHarness = defineComponent({
- components: {
- MarkdownMountProbe,
- },
- setup() {
- const mountedHtml = ref('')
+function createMarkdownHarness(markdown: string, label: string) {
+ return defineComponent({
+ components: {
+ MarkdownMountProbe,
+ },
+ setup() {
+ const mountedHtml = ref('')
- return {
- mountedHtml,
- }
- },
- template: `
-
-
- `,
-})
+ return {
+ label,
+ markdown,
+ mountedHtml,
+ }
+ },
+ template: `
+
+
+ `,
+ })
+}
describe('markdown renderer initial content', () => {
it('renders basic Markdown before mounted layout code reads the element', async () => {
@@ -62,8 +65,31 @@ describe('markdown renderer initial content', () => {
// The content jumped to its real height when the animation released its fixed height.
//
// We fixed this by rendering basic Markdown synchronously before optional rich processing.
- const screen = await render(MarkdownHarness)
+ const markdown = `### Roof Leak in Server Room
+
+*Anime style virtual AI girl waking up in a server room and noticing water from the ceiling.*
+
+> **Sharing**: Sending a quick sketch to our chat history...`
+ const screen = await render(createMarkdownHarness(markdown, 'initial-markdown-html'))
await expect.element(screen.getByLabelText('initial-markdown-html')).toHaveTextContent('
Roof Leak in Server Room
')
})
+
+ // https://github.com/moeru-ai/airi/discussions/2239
+ it('renders double-dollar math without consuming currency for Issue #2239', async () => {
+ // ROOT CAUSE:
+ //
+ // Single-dollar math can pair two currency signs. The rendered component
+ // then displays normal prose as a formula.
+ //
+ // We fixed this by keeping single dollars as text and using double dollars
+ // for inline math.
+ const markdown = 'Price is $5 and cost is $10.\n\nThe result is $$x^2$$.'
+ const screen = await render(createMarkdownHarness(markdown, 'chat-math-html'))
+ const html = screen.getByLabelText('chat-math-html')
+
+ await expect.element(html).toHaveTextContent('Price is $5 and cost is $10.')
+ await expect.element(html).toHaveTextContent('