@@ -26,7 +26,6 @@ interface LinkMap {
26
26
27
27
function formatDate ( dateString : string | undefined ) : string {
28
28
if ( ! dateString ) return "날짜 정보 없음" ;
29
-
30
29
try {
31
30
return new Date ( dateString ) . toLocaleDateString ( "ko-KR" ) ;
32
31
} catch ( error ) {
@@ -69,35 +68,55 @@ export default function MarkdownRenderer({
69
68
70
69
const processUncycloLinks = ( text : string ) => {
71
70
// 이스케이프된 링크 패턴을 정상 링크로 변환
72
- let processed = text . replace (
73
- / \\ \[ ( .* ?) \\ \] \( ( .* ?) \) / g,
74
- ( _ , linkText , href ) => {
75
- // 링크 텍스트는 그대로, href는 URI 인코딩
76
- return `[${ linkText } ](${ encodeURIComponent ( href ) } )` ;
77
- } ,
78
- ) ;
79
-
80
- // 위키링크 처리
71
+ let processed = text . replace ( / \\ ( \[ | \] | \( | \) ) / g, '$1' ) ;
72
+
73
+ // 위키링크 처리 - 정규식 개선
81
74
processed = processed . replace (
82
- / \[ \[ ( [ ^ | ] + ) (?: \| ( [ ^ \] ] + ) ) ? \] \] / g,
75
+ / \[ \[ ( [ ^ | \] ] + ) (?: \| ( [ ^ \] ] + ) ) ? \] \] / g,
83
76
( _ , path , label ) => {
84
77
// 경로 정규화
85
78
const cleanPath = path . replace ( / \. m d $ / , "" ) ;
86
79
// 표시할 이름이 없으면 경로의 마지막 부분을 사용
87
80
const displayName = label || cleanPath . split ( "/" ) . pop ( ) || cleanPath ;
88
-
89
81
// URI 인코딩 적용
90
82
const encodedPath = encodeURIComponent ( cleanPath ) ;
91
-
92
83
// 인코딩된 경로로 마크다운 링크 생성
93
84
return `[${ displayName } ](${ encodedPath } )` ;
94
85
} ,
95
86
) ;
96
-
97
87
return processed ;
98
88
} ;
99
89
90
+ // 콜아웃 블록 내부의 위키링크도 처리하기 위한 특별 처리
91
+ const processContentWithCallouts = ( content : string ) => {
92
+ // 콜아웃 패턴 - 공백 포함 ("> [!type]" 형식)
93
+ const calloutRegex = / ( > \s \[ ! .* ?\] .* ?(?: \n > .* ?) * ) (?: \n \n | $ ) / gs;
94
+
95
+ return content . replace ( calloutRegex , ( calloutBlock ) => {
96
+ // 전체 콜아웃 블록 내에서 모든 위키링크를 한 번에 처리
97
+ return calloutBlock . replace (
98
+ / ( - ) ? \[ \[ ( [ ^ | \] ] + ) (?: \| ( [ ^ \] ] + ) ) ? \] \] / g,
99
+ ( match , bulletPoint , path , label ) => {
100
+ // 이스케이프 문자 제거 및 경로 정규화
101
+ const cleanPath = path . replace ( / \\ | \\ .m d $ / , "" ) ;
102
+ // 표시할 이름이 없으면 경로의 마지막 부분을 사용
103
+ const displayName = label || cleanPath . split ( "/" ) . pop ( ) || cleanPath ;
104
+ // URI 인코딩 적용
105
+ const encodedPath = encodeURIComponent ( cleanPath ) ;
106
+ // 불릿 포인트가 있으면 유지
107
+ const prefix = bulletPoint || "" ;
108
+ // 인코딩된 경로로 마크다운 링크 생성
109
+ return `${ prefix } [${ displayName } ](${ encodedPath } )` ;
110
+ }
111
+ ) ;
112
+ } ) ;
113
+ } ;
114
+
115
+ // 처리 순서 변경: 위키링크 처리 전에 콜아웃 처리
100
116
const processedContent = processUncycloLinks ( content ) ;
117
+ const fullProcessedContent = processContentWithCallouts ( processedContent ) ;
118
+ // 최종 처리된 콘텐츠
119
+ const finalProcessedContent = fullProcessedContent ;
101
120
102
121
const components = {
103
122
h1 : ( { children } : { children ?: React . ReactNode } ) => (
@@ -113,7 +132,6 @@ export default function MarkdownRenderer({
113
132
</ Badge >
114
133
) ) }
115
134
</ div >
116
-
117
135
{ /* 날짜 정보 */ }
118
136
< div className = "flex flex-col sm:flex-row sm:justify-between text-xs sm:text-sm text-muted-foreground mb-6 sm:mb-8 pb-3 sm:pb-4 border-b" >
119
137
< div > 작성일: { formatDate ( published ) } </ div >
@@ -141,22 +159,18 @@ export default function MarkdownRenderer({
141
159
p : ( { children } : { children ?: React . ReactNode } ) => {
142
160
const hasBlockElement = React . Children . toArray ( children ) . some ( ( child ) => {
143
161
if ( ! React . isValidElement ( child ) ) return false ;
144
-
145
162
const childType = ( child as React . ReactElement ) . type ;
146
163
const isCustomImage = childType === components . img ;
147
-
148
164
const htmlElementType =
149
165
typeof childType === "string"
150
166
? childType
151
167
: ( childType as React . ComponentType ) . displayName ;
152
-
153
168
return (
154
169
isCustomImage ||
155
170
( htmlElementType &&
156
171
[ "div" , "img" , "pre" , "table" ] . includes ( htmlElementType ) )
157
172
) ;
158
173
} ) ;
159
-
160
174
return hasBlockElement ? (
161
175
< div className = "my-3 sm:my-4 md:my-5" > { children } </ div >
162
176
) : (
@@ -178,7 +192,6 @@ export default function MarkdownRenderer({
178
192
} ) {
179
193
const match = / l a n g u a g e - ( \w + ) / . exec ( className || "" ) ;
180
194
const code = String ( children ) . replace ( / \n $ / , "" ) ;
181
-
182
195
return ! inline && match ? (
183
196
< div className = "relative my-4 sm:my-6 md:my-8 rounded-lg overflow-hidden" >
184
197
< div className = "flex items-center justify-between bg-primary/10 text-primary px-3 sm:px-4 py-1 sm:py-2 text-xs sm:text-sm font-mono" >
@@ -197,7 +210,6 @@ export default function MarkdownRenderer({
197
210
{ copiedCode === code ? "복사됨" : "복사" }
198
211
</ Button >
199
212
</ div >
200
-
201
213
< div className = "overflow-auto max-w-full break-all whitespace-pre-wrap" >
202
214
< SyntaxHighlighter
203
215
style = { isDark ? nightOwl : nord }
@@ -261,7 +273,6 @@ export default function MarkdownRenderer({
261
273
) ,
262
274
a : ( { href, children } : { href ?: string ; children ?: React . ReactNode } ) => {
263
275
if ( ! href ) return < span > { children } </ span > ;
264
-
265
276
// 외부 링크 처리 (http로 시작하는 경우)
266
277
if ( href . startsWith ( "http" ) ) {
267
278
return (
@@ -275,7 +286,6 @@ export default function MarkdownRenderer({
275
286
</ a >
276
287
) ;
277
288
}
278
-
279
289
// 홈으로 가는 링크
280
290
if ( href === "/" ) {
281
291
return (
@@ -288,16 +298,13 @@ export default function MarkdownRenderer({
288
298
</ Link >
289
299
) ;
290
300
}
291
-
292
301
// 내부 링크 처리
293
302
if ( ! isMapLoaded )
294
303
return < span className = "text-muted-foreground" > { children } </ span > ;
295
-
296
304
// 디코딩 및 정규화
297
305
const decodedHref = decodeURIComponent ( href ) ;
298
306
const normalizedHref = decodedHref . replace ( / \. m d $ / , "" ) ;
299
307
const targetFileName = normalizedHref . split ( "/" ) . pop ( ) ;
300
-
301
308
// 링크맵에서 검색
302
309
for ( const [ key , value ] of Object . entries ( linkMap ) ) {
303
310
const srcFileName = key . replace ( / \. m d $ / , "" ) . split ( "/" ) . pop ( ) ;
@@ -313,7 +320,6 @@ export default function MarkdownRenderer({
313
320
) ;
314
321
}
315
322
}
316
-
317
323
// 발행되지 않은 문서 링크
318
324
return (
319
325
< span
@@ -380,7 +386,7 @@ export default function MarkdownRenderer({
380
386
return (
381
387
< div className = "prose-custom" >
382
388
< ReactMarkdown components = { components } remarkPlugins = { [ remarkCallout ] } >
383
- { processedContent }
389
+ { finalProcessedContent }
384
390
</ ReactMarkdown >
385
391
</ div >
386
392
) ;
0 commit comments