diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1d1034c7..8b795423 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,7 +1,6 @@
name: CI
on:
push:
- branches: ["master"]
pull_request:
jobs:
lint:
diff --git a/src/components/Markdown/Markdown.test.tsx b/src/components/Markdown/Markdown.test.tsx
index fae194c2..64767c1e 100644
--- a/src/components/Markdown/Markdown.test.tsx
+++ b/src/components/Markdown/Markdown.test.tsx
@@ -103,6 +103,14 @@ describe('Markdown', () => {
const { getByText } = render()
expect(getByText('This is a blockquote.')).toBeDefined()
})
+
+ it('soft line break creates a break tag', () => {
+ // Models often expect newlines to be presented as line breaks
+ const text = 'Line one\nLine two'
+ const { container } = render()
+ expect(container.innerHTML).toContain('Line one
Line two')
+ expect(container.querySelector('br')).toBeDefined()
+ })
})
describe('Markdown horizontal rules', () => {
diff --git a/src/components/Markdown/Markdown.tsx b/src/components/Markdown/Markdown.tsx
index ffc7ea9b..0a3ab25d 100644
--- a/src/components/Markdown/Markdown.tsx
+++ b/src/components/Markdown/Markdown.tsx
@@ -5,8 +5,14 @@ interface MarkdownProps {
className?: string
}
+// Enable soft line breaks (single newline becomes
)
+// Models often expect newlines to be presented as line breaks.
+/* eslint-disable @typescript-eslint/no-unnecessary-condition */
+const softLineBreaks = true
+
type Token =
| { type: 'text', content: string }
+ | { type: 'break' }
| { type: 'bold', children: Token[] }
| { type: 'italic', children: Token[] }
| { type: 'code', content: string }
@@ -152,7 +158,7 @@ function parseMarkdown(text: string): Token[] {
}
tokens.push({
type: 'paragraph',
- children: parseInline(paraLines.join(' ')),
+ children: parseInline(paraLines.join('\n')),
})
}
@@ -249,6 +255,9 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]
return [items, i]
}
+/**
+ * Convert inline tokens back to plain text (for alt text, link text, etc.)
+ */
function tokensToString(tokens: Token[]): string {
return tokens
.map(token => {
@@ -256,6 +265,8 @@ function tokensToString(tokens: Token[]): string {
case 'text':
case 'code':
return token.content
+ case 'break':
+ return ' '
case 'bold':
case 'italic':
case 'link':
@@ -272,6 +283,10 @@ function parseInline(text: string): Token[] {
return tokens
}
+/**
+ * Recursively parse inline markdown elements.
+ * Returns a tuple of [tokens, charsConsumed].
+ */
function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
const tokens: Token[] = []
let i = 0
@@ -417,6 +432,7 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
let j = i
while (
j < text.length &&
+ (text[j] !== '\n' || !softLineBreaks) &&
text[j] !== '`' &&
!(text.startsWith('**', j) || text.startsWith('__', j)) &&
text[j] !== '*' &&
@@ -431,6 +447,13 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
j++
}
+ // soft line break
+ if (softLineBreaks && text[i] === '\n') {
+ tokens.push({ type: 'break' })
+ i++
+ continue
+ }
+
if (j === i) {
// didn't consume anything – treat the single char literally
tokens.push({ type: 'text', content: text[i] ?? '' })
@@ -444,7 +467,9 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
return [tokens, i]
}
-/** Split a table row, trimming outer pipes and whitespace */
+/**
+ * Split a table row, trimming outer pipes and whitespace
+ */
function splitTableRow(row: string): string[] {
const cells: string[] = []
let current = ''
@@ -504,12 +529,17 @@ function isClosingAsterisk(text: string, pos: number): boolean {
return !/\s/.test(prev) // prev char is not whitespace
}
+/**
+ * Render tokens to React elements.
+ */
function renderTokens(tokens: Token[], keyPrefix = ''): ReactNode[] {
return tokens.map((token, index) => {
const key = `${keyPrefix}${index}`
switch (token.type) {
case 'text':
return token.content
+ case 'break':
+ return createElement('br', { key })
case 'code':
return createElement('code', { key }, token.content)
case 'bold':