Skip to content

Commit 4c66d10

Browse files
authored
Markdown: newline adds a break (#264)
1 parent 1e5ac49 commit 4c66d10

File tree

3 files changed

+40
-3
lines changed

3 files changed

+40
-3
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
name: CI
22
on:
33
push:
4-
branches: ["master"]
54
pull_request:
65
jobs:
76
lint:

src/components/Markdown/Markdown.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ describe('Markdown', () => {
103103
const { getByText } = render(<Markdown text={text} />)
104104
expect(getByText('This is a blockquote.')).toBeDefined()
105105
})
106+
107+
it('soft line break creates a break tag', () => {
108+
// Models often expect newlines to be presented as line breaks
109+
const text = 'Line one\nLine two'
110+
const { container } = render(<Markdown text={text} />)
111+
expect(container.innerHTML).toContain('Line one<br>Line two')
112+
expect(container.querySelector('br')).toBeDefined()
113+
})
106114
})
107115

108116
describe('Markdown horizontal rules', () => {

src/components/Markdown/Markdown.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ interface MarkdownProps {
55
className?: string
66
}
77

8+
// Enable soft line breaks (single newline becomes <br>)
9+
// Models often expect newlines to be presented as line breaks.
10+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
11+
const softLineBreaks = true
12+
813
type Token =
914
| { type: 'text', content: string }
15+
| { type: 'break' }
1016
| { type: 'bold', children: Token[] }
1117
| { type: 'italic', children: Token[] }
1218
| { type: 'code', content: string }
@@ -152,7 +158,7 @@ function parseMarkdown(text: string): Token[] {
152158
}
153159
tokens.push({
154160
type: 'paragraph',
155-
children: parseInline(paraLines.join(' ')),
161+
children: parseInline(paraLines.join('\n')),
156162
})
157163
}
158164

@@ -249,13 +255,18 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]
249255
return [items, i]
250256
}
251257

258+
/**
259+
* Convert inline tokens back to plain text (for alt text, link text, etc.)
260+
*/
252261
function tokensToString(tokens: Token[]): string {
253262
return tokens
254263
.map(token => {
255264
switch (token.type) {
256265
case 'text':
257266
case 'code':
258267
return token.content
268+
case 'break':
269+
return ' '
259270
case 'bold':
260271
case 'italic':
261272
case 'link':
@@ -272,6 +283,10 @@ function parseInline(text: string): Token[] {
272283
return tokens
273284
}
274285

286+
/**
287+
* Recursively parse inline markdown elements.
288+
* Returns a tuple of [tokens, charsConsumed].
289+
*/
275290
function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
276291
const tokens: Token[] = []
277292
let i = 0
@@ -417,6 +432,7 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
417432
let j = i
418433
while (
419434
j < text.length &&
435+
(text[j] !== '\n' || !softLineBreaks) &&
420436
text[j] !== '`' &&
421437
!(text.startsWith('**', j) || text.startsWith('__', j)) &&
422438
text[j] !== '*' &&
@@ -431,6 +447,13 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
431447
j++
432448
}
433449

450+
// soft line break
451+
if (softLineBreaks && text[i] === '\n') {
452+
tokens.push({ type: 'break' })
453+
i++
454+
continue
455+
}
456+
434457
if (j === i) {
435458
// didn't consume anything – treat the single char literally
436459
tokens.push({ type: 'text', content: text[i] ?? '' })
@@ -444,7 +467,9 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
444467
return [tokens, i]
445468
}
446469

447-
/** Split a table row, trimming outer pipes and whitespace */
470+
/**
471+
* Split a table row, trimming outer pipes and whitespace
472+
*/
448473
function splitTableRow(row: string): string[] {
449474
const cells: string[] = []
450475
let current = ''
@@ -504,12 +529,17 @@ function isClosingAsterisk(text: string, pos: number): boolean {
504529
return !/\s/.test(prev) // prev char is not whitespace
505530
}
506531

532+
/**
533+
* Render tokens to React elements.
534+
*/
507535
function renderTokens(tokens: Token[], keyPrefix = ''): ReactNode[] {
508536
return tokens.map((token, index) => {
509537
const key = `${keyPrefix}${index}`
510538
switch (token.type) {
511539
case 'text':
512540
return token.content
541+
case 'break':
542+
return createElement('br', { key })
513543
case 'code':
514544
return createElement('code', { key }, token.content)
515545
case 'bold':

0 commit comments

Comments
 (0)