-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocs-callout.tsx
More file actions
122 lines (97 loc) · 3.01 KB
/
docs-callout.tsx
File metadata and controls
122 lines (97 loc) · 3.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import type {ComponentPropsWithoutRef, ReactElement, ReactNode} from 'react'
import {Children, cloneElement, isValidElement} from 'react'
type CalloutTone = 'note' | 'tip' | 'important' | 'warning' | 'caution'
type BlockquoteProps = ComponentPropsWithoutRef<'blockquote'>
const CALLOUT_PATTERN = /^\s*\[!(note|tip|important|warning|caution)\]\s*/i
const CALLOUT_TONES = new Set<CalloutTone>(['note', 'tip', 'important', 'warning', 'caution'])
const CALLOUT_LABELS: Record<CalloutTone, string> = {
note: 'Note',
tip: 'Tip',
important: 'Important',
warning: 'Warning',
caution: 'Caution'
}
function extractText(node: ReactNode): string {
if (typeof node === 'string') {
return node
}
if (Array.isArray(node)) {
return node.map(extractText).join('')
}
if (isValidElement(node)) {
return extractText((node as ReactElement<{children?: ReactNode}>).props.children)
}
return ''
}
function getMeaningfulChildren(children: ReactNode): ReactNode[] {
return Children.toArray(children).filter(child => {
if (typeof child !== 'string') {
return true
}
return child.trim() !== ''
})
}
function stripMarkerFromChildren(children: ReactNode): ReactNode {
const items = getMeaningfulChildren(children)
const strippedItems: ReactNode[] = []
for (const [index, item] of items.entries()) {
if (index !== 0) {
strippedItems.push(item)
continue
}
if (typeof item === 'string') {
strippedItems.push(item.replace(CALLOUT_PATTERN, ''))
continue
}
if (!isValidElement(item)) {
strippedItems.push(item)
continue
}
const element = item as ReactElement<{children?: ReactNode}>
const text = extractText(element.props.children)
if (!CALLOUT_PATTERN.test(text)) {
strippedItems.push(item)
continue
}
strippedItems.push(cloneElement(element, {
...element.props,
children: text.replace(CALLOUT_PATTERN, '')
}))
}
return strippedItems
}
function isCalloutTone(value: string | undefined): value is CalloutTone {
return value != null && CALLOUT_TONES.has(value as CalloutTone)
}
function resolveCalloutTone(children: ReactNode): CalloutTone | null {
const firstChild = getMeaningfulChildren(children)[0]
const firstText = extractText(firstChild).trimStart()
const matched = CALLOUT_PATTERN.exec(firstText)?.[1]?.toLowerCase()
if (isCalloutTone(matched)) {
return matched
}
return null
}
export function DocsBlockquote({children, className, ...props}: BlockquoteProps) {
const tone = resolveCalloutTone(children)
if (tone == null) {
return (
<blockquote className={className} {...props}>
{children}
</blockquote>
)
}
return (
<div
className={[
'docs-callout',
`docs-callout--${tone}`,
className ?? ''
].join(' ').trim()}
data-tone={tone}
>
<div className="docs-callout__title">{CALLOUT_LABELS[tone]}</div>
<div className="docs-callout__content">{stripMarkerFromChildren(children)}</div>
</div>
)
}