Skip to content

Commit 1ca5e42

Browse files
committed
fix: cleaner implementation, with bolded default options
1 parent b8de23a commit 1ca5e42

File tree

2 files changed

+160
-36
lines changed

2 files changed

+160
-36
lines changed

cli/src/utils/__tests__/markdown-renderer.test.tsx

Lines changed: 126 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,73 @@ const flattenNodes = (input: React.ReactNode): React.ReactNode[] => {
3939
const flattenChildren = (value: React.ReactNode): React.ReactNode[] =>
4040
flattenNodes(value)
4141

42+
// Helper to find all elements matching a predicate (recursively)
43+
const findAllElements = (
44+
input: React.ReactNode,
45+
predicate: (element: React.ReactElement) => boolean,
46+
): React.ReactElement[] => {
47+
const results: React.ReactElement[] = []
48+
49+
const visit = (value: React.ReactNode): void => {
50+
if (
51+
value === null ||
52+
value === undefined ||
53+
typeof value === 'boolean' ||
54+
typeof value === 'string' ||
55+
typeof value === 'number'
56+
) {
57+
return
58+
}
59+
60+
if (Array.isArray(value)) {
61+
value.forEach(visit)
62+
return
63+
}
64+
65+
if (React.isValidElement(value)) {
66+
// Check if this element matches
67+
if (predicate(value)) {
68+
results.push(value)
69+
}
70+
// Always recurse into children
71+
if (value.props?.children) {
72+
visit(value.props.children)
73+
}
74+
}
75+
}
76+
77+
visit(input)
78+
return results
79+
}
80+
81+
// Helper to extract all text content recursively
82+
const getAllTextContent = (input: React.ReactNode): string => {
83+
const texts: string[] = []
84+
85+
const visit = (value: React.ReactNode): void => {
86+
if (value === null || value === undefined || typeof value === 'boolean') {
87+
return
88+
}
89+
90+
if (typeof value === 'string' || typeof value === 'number') {
91+
texts.push(String(value))
92+
return
93+
}
94+
95+
if (Array.isArray(value)) {
96+
value.forEach(visit)
97+
return
98+
}
99+
100+
if (React.isValidElement(value)) {
101+
visit(value.props?.children)
102+
}
103+
}
104+
105+
visit(input)
106+
return texts.join('')
107+
}
108+
42109
describe('markdown renderer', () => {
43110
test('renders bold and italic emphasis', () => {
44111
const output = renderMarkdown('Hello **bold** and *italic*!')
@@ -343,18 +410,9 @@ a) (DEFAULT) Implement webhook system first (most flexible for enterprise custom
343410
b) Focus on email/in-app notifications first (simpler to implement)`
344411

345412
const output = renderMarkdown(markdown)
346-
const nodes = flattenNodes(output)
347413

348-
// Convert all nodes to text to check indentation
349-
const textContent = nodes
350-
.map((node) => {
351-
if (typeof node === 'string') return node
352-
if (React.isValidElement(node)) {
353-
return flattenChildren(node.props.children).join('')
354-
}
355-
return ''
356-
})
357-
.join('')
414+
// Convert to text content (recursively extracts all text including from nested spans)
415+
const textContent = getAllTextContent(output)
358416

359417
// Lettered items should be indented (3 spaces when under numbered lists)
360418
expect(textContent).toContain(' a) (DEFAULT) PostgreSQL')
@@ -428,17 +486,9 @@ b) Custom configuration
428486
c) Advanced configuration`
429487

430488
const output = renderMarkdown(markdown)
431-
const nodes = flattenNodes(output)
432489

433-
const textContent = nodes
434-
.map((node) => {
435-
if (typeof node === 'string') return node
436-
if (React.isValidElement(node)) {
437-
return flattenChildren(node.props.children).join('')
438-
}
439-
return ''
440-
})
441-
.join('')
490+
// Convert to text content (recursively extracts all text including from nested spans)
491+
const textContent = getAllTextContent(output)
442492

443493
// Should preserve DEFAULT markers and apply indentation (3 spaces under list)
444494
expect(textContent).toContain(' a) (DEFAULT) Standard')
@@ -586,5 +636,60 @@ Conclusion text at the end.`
586636
expect(textContent).not.toContain(' a) something')
587637
expect(textContent).toContain('a) something in the middle')
588638
})
639+
640+
test('makes DEFAULT options bold', () => {
641+
const markdown = `1. Choose your option:
642+
a) (DEFAULT) Standard configuration
643+
b) Custom configuration
644+
c) Advanced configuration`
645+
646+
const output = renderMarkdown(markdown)
647+
648+
// Find all span elements with BOLD attribute (recursively)
649+
const boldSpans = findAllElements(
650+
output,
651+
(el) => el.type === 'span' && el.props.attributes === TextAttributes.BOLD
652+
)
653+
654+
// Should have at least one bold span
655+
expect(boldSpans.length).toBeGreaterThan(0)
656+
657+
// Check that bold span contains DEFAULT text
658+
const boldTexts = boldSpans.map((span) =>
659+
flattenChildren(span.props.children).join('')
660+
)
661+
const hasDEFAULT = boldTexts.some((text) => text.includes('(DEFAULT)'))
662+
expect(hasDEFAULT).toBe(true)
663+
})
664+
665+
test('bolds multiple DEFAULT options', () => {
666+
const markdown = `1. First question:
667+
a) (DEFAULT) Option A
668+
b) Option B
669+
2. Second question:
670+
a) (DEFAULT) Another default
671+
b) Non-default option`
672+
673+
const output = renderMarkdown(markdown)
674+
675+
// Find all span elements with BOLD attribute (recursively)
676+
const boldSpans = findAllElements(
677+
output,
678+
(el) => el.type === 'span' && el.props.attributes === TextAttributes.BOLD
679+
)
680+
681+
// Should have at least 2 bold spans
682+
expect(boldSpans.length).toBeGreaterThanOrEqual(2)
683+
684+
// Both bold spans should contain DEFAULT text
685+
const boldTexts = boldSpans.map((span) =>
686+
flattenChildren(span.props.children).join('')
687+
)
688+
const defaultCount = boldTexts.filter((text) =>
689+
text.includes('(DEFAULT)')
690+
).length
691+
692+
expect(defaultCount).toBeGreaterThanOrEqual(2)
693+
})
589694
})
590695
})

cli/src/utils/markdown-renderer.tsx

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -729,33 +729,52 @@ const renderNode = (
729729
)
730730
let nodes = [...children]
731731

732-
// Add indentation for lettered sub-items (a), b), c), etc.)
733-
// Process text nodes to add indentation at the start and after each newline
734-
// Check both root and listItem context
732+
// Check if this paragraph contains lettered sub-items (a), b), c), etc.)
733+
// If so, wrap each line that's a lettered item in a span with indentation
735734
if (parentType === 'root' || parentType === 'listItem') {
736735
const processedNodes: ReactNode[] = []
736+
const indentSpaces = parentType === 'listItem' ? ' ' : ' '
737737

738738
for (const child of nodes) {
739739
if (typeof child === 'string') {
740-
// Split by newlines and add indentation for lettered items
740+
// Split by newlines and wrap each lettered item in a span
741741
const lines = child.split('\n')
742-
const processedLines: string[] = []
743-
744742
for (let i = 0; i < lines.length; i++) {
745743
const line = lines[i]
746-
const needsIndent = line.match(/^([a-z])\)\s/)
747-
748-
if (needsIndent) {
749-
// Use 3 spaces for sub-items under list items, 6 for root level
750-
const indent = parentType === 'listItem' ? ' ' : ' '
751-
processedLines.push(indent + line)
744+
const isLetteredItem = line.match(/^([a-z])\)\s/)
745+
746+
if (isLetteredItem) {
747+
// Check if this option is marked as DEFAULT
748+
const isDefault = line.includes('(DEFAULT)')
749+
750+
// Wrap this lettered item in a span with indentation
751+
// If DEFAULT, make the entire line bold
752+
const content = isDefault ? (
753+
<span key={state.nextKey()} attributes={TextAttributes.BOLD}>
754+
{line}
755+
</span>
756+
) : (
757+
line
758+
)
759+
760+
processedNodes.push(
761+
<span key={state.nextKey()}>
762+
{indentSpaces}
763+
{content}
764+
</span>
765+
)
752766
} else {
753-
processedLines.push(line)
767+
// Not a lettered item, keep as is
768+
processedNodes.push(line)
754769
}
755-
}
756770

757-
processedNodes.push(processedLines.join('\n'))
771+
// Add newline between lines (except after last line)
772+
if (i < lines.length - 1) {
773+
processedNodes.push('\n')
774+
}
775+
}
758776
} else {
777+
// Non-string node (React element), keep as is
759778
processedNodes.push(child)
760779
}
761780
}

0 commit comments

Comments
 (0)